// Copyright (c) 2012 The Chromium Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. // Copyright (c) 2012 The Chromium Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. /** * Sets the width (in pixels) on a DOM node. * @param {!HtmlNode} node The node whose width is being set. * @param {number} widthPx The width in pixels. */ function setNodeWidth(node, widthPx) { node.style.width = widthPx.toFixed(0) + 'px'; } /** * Sets the height (in pixels) on a DOM node. * @param {!HtmlNode} node The node whose height is being set. * @param {number} heightPx The height in pixels. */ function setNodeHeight(node, heightPx) { node.style.height = heightPx.toFixed(0) + 'px'; } /** * Sets the position and size of a DOM node (in pixels). * @param {!HtmlNode} node The node being positioned. * @param {number} leftPx The left position in pixels. * @param {number} topPx The top position in pixels. * @param {number} widthPx The width in pixels. * @param {number} heightPx The height in pixels. */ function setNodePosition(node, leftPx, topPx, widthPx, heightPx) { node.style.left = leftPx.toFixed(0) + 'px'; node.style.top = topPx.toFixed(0) + 'px'; setNodeWidth(node, widthPx); setNodeHeight(node, heightPx); } /** * Sets the visibility for a DOM node. * @param {!HtmlNode} node The node being positioned. * @param {boolean} isVisible Whether to show the node or not. */ function setNodeDisplay(node, isVisible) { node.style.display = isVisible ? '' : 'none'; } /** * Adds a node to |parentNode|, of type |tagName|. * @param {!HtmlNode} parentNode The node that will be the parent of the new * element. * @param {string} tagName the tag name of the new element. * @return {!HtmlElement} The newly created element. */ function addNode(parentNode, tagName) { var elem = parentNode.ownerDocument.createElement(tagName); parentNode.appendChild(elem); return elem; } /** * Adds |text| to node |parentNode|. * @param {!HtmlNode} parentNode The node to add text to. * @param {string} text The text to be added. * @return {!Object} The newly created text node. */ function addTextNode(parentNode, text) { var textNode = parentNode.ownerDocument.createTextNode(text); parentNode.appendChild(textNode); return textNode; } /** * Adds a node to |parentNode|, of type |tagName|. Then adds * |text| to the new node. * @param {!HtmlNode} parentNode The node that will be the parent of the new * element. * @param {string} tagName the tag name of the new element. * @param {string} text The text to be added. * @return {!HtmlElement} The newly created element. */ function addNodeWithText(parentNode, tagName, text) { var elem = parentNode.ownerDocument.createElement(tagName); parentNode.appendChild(elem); addTextNode(elem, text); return elem; } /** * Returns the key such that map[key] == value, or the string '?' if * there is no such key. * @param {!Object} map The object being used as a lookup table. * @param {Object} value The value to be found in |map|. * @return {string} The key for |value|, or '?' if there is no such key. */ function getKeyWithValue(map, value) { for (var key in map) { if (map[key] == value) return key; } return '?'; } /** * Returns a new map with the keys and values of the input map inverted. * @param {!Object} map The object to have its keys and values reversed. Not * modified by the function call. * @return {Object} The new map with the reversed keys and values. */ function makeInverseMap(map) { var reversed = {}; for (var key in map) reversed[map[key]] = key; return reversed; } /** * Looks up |key| in |map|, and returns the resulting entry, if there is one. * Otherwise, returns |key|. Intended primarily for use with incomplete * tables, and for reasonable behavior with system enumerations that may be * extended in the future. * @param {!Object} map The table in which |key| is looked up. * @param {string} key The key to look up. * @return {Object|string} map[key], if it exists, or |key| if it doesn't. */ function tryGetValueWithKey(map, key) { if (key in map) return map[key]; return key; } /** * Builds a string by repeating |str| |count| times. * @param {string} str The string to be repeated. * @param {number} count The number of times to repeat |str|. * @return {string} The constructed string */ function makeRepeatedString(str, count) { var out = []; for (var i = 0; i < count; ++i) out.push(str); return out.join(''); } /** * Clones a basic POD object. Only a new top level object will be cloned. It * will continue to reference the same values as the original object. * @param {Object} object The object to be cloned. * @return {Object} A copy of |object|. */ function shallowCloneObject(object) { if (!(object instanceof Object)) return object; var copy = {}; for (var key in object) { copy[key] = object[key]; } return copy; } /** * Helper to make sure singleton classes are not instantiated more than once. * @param {Function} ctor The constructor function being checked. */ function assertFirstConstructorCall(ctor) { // This is the variable which is set by cr.addSingletonGetter(). if (ctor.hasCreateFirstInstance_) { throw Error('The class ' + ctor.name + ' is a singleton, and should ' + 'only be accessed using ' + ctor.name + '.getInstance().'); } ctor.hasCreateFirstInstance_ = true; } function hasTouchScreen() { return 'ontouchstart' in window; } // Copyright (c) 2012 The Chromium Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. /** * TablePrinter is a helper to format a table as ASCII art or an HTML table. * * Usage: call addRow() and addCell() repeatedly to specify the data. * * addHeaderCell() can optionally be called to specify header cells for a * single header row. The header row appears at the top of an HTML formatted * table, and uses thead and th tags. In ascii tables, the header is separated * from the table body by a partial row of dashes. * * setTitle() can optionally be used to set a title that is displayed before * the header row. In HTML tables, it uses the title class and in ascii tables * it's between two rows of dashes. * * Once all the fields have been input, call toText() to format it as text or * toHTML() to format it as HTML. */ var TablePrinter = (function() { 'use strict'; /** * @constructor */ function TablePrinter() { this.rows_ = []; this.hasHeaderRow_ = false; this.title_ = null; // Number of cells automatically added at the start of new rows. this.newRowCellIndent_ = 0; } TablePrinter.prototype = { /** * Sets the number of blank cells to add after each call to addRow. */ setNewRowCellIndent: function(newRowCellIndent) { this.newRowCellIndent_ = newRowCellIndent; }, /** * Starts a new row. */ addRow: function() { this.rows_.push([]); for (var i = 0; i < this.newRowCellIndent_; ++i) this.addCell(''); }, /** * Adds a column to the current row, setting its value to cellText. * * @return {!TablePrinterCell} the cell that was added. */ addCell: function(cellText) { var r = this.rows_[this.rows_.length - 1]; var cell = new TablePrinterCell(cellText); r.push(cell); return cell; }, /** * Sets the title displayed at the top of a table. Titles are optional. */ setTitle: function(title) { this.title_ = title; }, /** * Adds a header row, if not already present, and adds a new column to it, * setting its contents to |headerText|. * * @return {!TablePrinterCell} the cell that was added. */ addHeaderCell: function(headerText) { // Insert empty new row at start of |rows_| if currently no header row. if (!this.hasHeaderRow_) { this.rows_.splice(0, 0, []); this.hasHeaderRow_ = true; } var cell = new TablePrinterCell(headerText); this.rows_[0].push(cell); return cell; }, /** * Returns the maximum number of columns this table contains. */ getNumColumns: function() { var numColumns = 0; for (var i = 0; i < this.rows_.length; ++i) { numColumns = Math.max(numColumns, this.rows_[i].length); } return numColumns; }, /** * Returns the cell at position (rowIndex, columnIndex), or null if there is * no such cell. */ getCell_: function(rowIndex, columnIndex) { if (rowIndex >= this.rows_.length) return null; var row = this.rows_[rowIndex]; if (columnIndex >= row.length) return null; return row[columnIndex]; }, /** * Returns true if searchString can be found entirely within a cell. * Case insensitive. * * @param {string} string String to search for, must be lowercase. * @return {boolean} True if some cell contains searchString. */ search: function(searchString) { var numColumns = this.getNumColumns(); for (var r = 0; r < this.rows_.length; ++r) { for (var c = 0; c < numColumns; ++c) { var cell = this.getCell_(r, c); if (!cell) continue; if (cell.text.toLowerCase().indexOf(searchString) != -1) return true; } } return false; }, /** * Prints a formatted text representation of the table data to the * node |parent|. |spacing| indicates number of extra spaces, if any, * to add between columns. */ toText: function(spacing, parent) { var pre = addNode(parent, 'pre'); var numColumns = this.getNumColumns(); // Figure out the maximum width of each column. var columnWidths = []; columnWidths.length = numColumns; for (var i = 0; i < numColumns; ++i) columnWidths[i] = 0; // If header row is present, temporarily add a spacer row to |rows_|. if (this.hasHeaderRow_) { var headerSpacerRow = []; for (var c = 0; c < numColumns; ++c) { var cell = this.getCell_(0, c); if (!cell) continue; var spacerStr = makeRepeatedString('-', cell.text.length); headerSpacerRow.push(new TablePrinterCell(spacerStr)); } this.rows_.splice(1, 0, headerSpacerRow); } var numRows = this.rows_.length; for (var c = 0; c < numColumns; ++c) { for (var r = 0; r < numRows; ++r) { var cell = this.getCell_(r, c); if (cell && !cell.allowOverflow) { columnWidths[c] = Math.max(columnWidths[c], cell.text.length); } } } var out = []; // Print title, if present. if (this.title_) { var titleSpacerStr = makeRepeatedString('-', this.title_.length); out.push(titleSpacerStr); out.push('\n'); out.push(this.title_); out.push('\n'); out.push(titleSpacerStr); out.push('\n'); } // Print each row. var spacingStr = makeRepeatedString(' ', spacing); for (var r = 0; r < numRows; ++r) { for (var c = 0; c < numColumns; ++c) { var cell = this.getCell_(r, c); if (cell) { // Pad the cell with spaces to make it fit the maximum column width. var padding = columnWidths[c] - cell.text.length; var paddingStr = makeRepeatedString(' ', padding); if (cell.alignRight) out.push(paddingStr); if (cell.link) { // Output all previous text, and clear |out|. addTextNode(pre, out.join('')); out = []; var linkNode = addNodeWithText(pre, 'a', cell.text); linkNode.href = cell.link; } else { out.push(cell.text); } if (!cell.alignRight) out.push(paddingStr); out.push(spacingStr); } } out.push('\n'); } // Remove spacer row under the header row, if one was added. if (this.hasHeaderRow_) this.rows_.splice(1, 1); addTextNode(pre, out.join('')); }, /** * Adds a new HTML table to the node |parent| using the specified style. */ toHTML: function(parent, style) { var numRows = this.rows_.length; var numColumns = this.getNumColumns(); var table = addNode(parent, 'table'); table.setAttribute('class', style); var thead = addNode(table, 'thead'); var tbody = addNode(table, 'tbody'); // Add title, if needed. if (this.title_) { var tableTitleRow = addNode(thead, 'tr'); var tableTitle = addNodeWithText(tableTitleRow, 'th', this.title_); tableTitle.colSpan = numColumns; tableTitle.classList.add('title'); } // Fill table body, adding header row first, if needed. for (var r = 0; r < numRows; ++r) { var cellType; var row; if (r == 0 && this.hasHeaderRow_) { row = addNode(thead, 'tr'); cellType = 'th'; } else { row = addNode(tbody, 'tr'); cellType = 'td'; } for (var c = 0; c < numColumns; ++c) { var cell = this.getCell_(r, c); if (cell) { var tableCell = addNode(row, cellType, cell.text); if (cell.alignRight) tableCell.alignRight = true; // If allowing overflow on the rightmost cell of a row, // make the cell span the rest of the columns. Otherwise, // ignore the flag. if (cell.allowOverflow && !this.getCell_(r, c + 1)) tableCell.colSpan = numColumns - c; if (cell.link) { var linkNode = addNodeWithText(tableCell, 'a', cell.text); linkNode.href = cell.link; } else { addTextNode(tableCell, cell.text); } } } } return table; } }; /** * Links are only used in HTML tables. */ function TablePrinterCell(value) { this.text = '' + value; this.link = null; this.alignRight = false; this.allowOverflow = false; } return TablePrinter; })(); // Copyright (c) 2012 The Chromium Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. /** * Base class to represent a "view". A view is an absolutely positioned box on * the page. */ var View = (function() { 'use strict'; /** * @constructor */ function View() { this.isVisible_ = true; } View.prototype = { /** * Called to reposition the view on the page. Measurements are in pixels. */ setGeometry: function(left, top, width, height) { this.left_ = left; this.top_ = top; this.width_ = width; this.height_ = height; }, /** * Called to show/hide the view. */ show: function(isVisible) { this.isVisible_ = isVisible; }, isVisible: function() { return this.isVisible_; }, /** * Method of the observer class. * * Called to check if an observer needs the data it is * observing to be actively updated. */ isActive: function() { return this.isVisible(); }, getLeft: function() { return this.left_; }, getTop: function() { return this.top_; }, getWidth: function() { return this.width_; }, getHeight: function() { return this.height_; }, getRight: function() { return this.getLeft() + this.getWidth(); }, getBottom: function() { return this.getTop() + this.getHeight(); }, setParameters: function(params) {}, /** * Called when loading a log file, after clearing all events, but before * loading the new ones. |polledData| contains the data from all * PollableData helpers. |tabData| contains the data for the particular * tab. |logDump| is the entire log dump, which includes the other two * values. It's included separately so most views don't have to depend on * its specifics. */ onLoadLogStart: function(polledData, tabData, logDump) { }, /** * Called as the final step of loading a log file. Arguments are the same * as onLoadLogStart. Returns true to indicate the tab should be shown, * false otherwise. */ onLoadLogFinish: function(polledData, tabData, logDump) { return false; } }; return View; })(); //----------------------------------------------------------------------------- /** * DivView is an implementation of View that wraps a DIV. */ var DivView = (function() { 'use strict'; // We inherit from View. var superClass = View; /** * @constructor */ function DivView(divId) { // Call superclass's constructor. superClass.call(this); this.node_ = $(divId); if (!this.node_) throw new Error('Element ' + divId + ' not found'); // Initialize the default values to those of the DIV. this.width_ = this.node_.offsetWidth; this.height_ = this.node_.offsetHeight; this.isVisible_ = this.node_.style.display != 'none'; } DivView.prototype = { // Inherit the superclass's methods. __proto__: superClass.prototype, setGeometry: function(left, top, width, height) { superClass.prototype.setGeometry.call(this, left, top, width, height); this.node_.style.position = 'absolute'; setNodePosition(this.node_, left, top, width, height); }, show: function(isVisible) { superClass.prototype.show.call(this, isVisible); setNodeDisplay(this.node_, isVisible); }, /** * Returns the wrapped DIV */ getNode: function() { return this.node_; } }; return DivView; })(); //----------------------------------------------------------------------------- /** * Implementation of View that sizes its child to fit the entire window. * * @param {!View} childView The child view. */ var WindowView = (function() { 'use strict'; // We inherit from View. var superClass = View; /** * @constructor */ function WindowView(childView) { // Call superclass's constructor. superClass.call(this); this.childView_ = childView; window.addEventListener('resize', this.resetGeometry.bind(this), true); } WindowView.prototype = { // Inherit the superclass's methods. __proto__: superClass.prototype, setGeometry: function(left, top, width, height) { superClass.prototype.setGeometry.call(this, left, top, width, height); this.childView_.setGeometry(left, top, width, height); }, show: function() { superClass.prototype.show.call(this, isVisible); this.childView_.show(isVisible); }, resetGeometry: function() { this.setGeometry(0, 0, window.innerWidth, window.innerHeight); } }; return WindowView; })(); /** * View that positions two views vertically. The top view should be * fixed-height, and the bottom view will fill the remainder of the space. * * +-----------------------------------+ * | topView | * +-----------------------------------+ * | | * | | * | | * | bottomView | * | | * | | * | | * | | * +-----------------------------------+ */ var VerticalSplitView = (function() { 'use strict'; // We inherit from View. var superClass = View; /** * @param {!View} topView The top view. * @param {!View} bottomView The bottom view. * @constructor */ function VerticalSplitView(topView, bottomView) { // Call superclass's constructor. superClass.call(this); this.topView_ = topView; this.bottomView_ = bottomView; } VerticalSplitView.prototype = { // Inherit the superclass's methods. __proto__: superClass.prototype, setGeometry: function(left, top, width, height) { superClass.prototype.setGeometry.call(this, left, top, width, height); var fixedHeight = this.topView_.getHeight(); this.topView_.setGeometry(left, top, width, fixedHeight); this.bottomView_.setGeometry( left, top + fixedHeight, width, height - fixedHeight); }, show: function(isVisible) { superClass.prototype.show.call(this, isVisible); this.topView_.show(isVisible); this.bottomView_.show(isVisible); } }; return VerticalSplitView; })(); /** * View that positions two views horizontally. The left view should be * fixed-width, and the right view will fill the remainder of the space. * * +----------+--------------------------+ * | | | * | | | * | | | * | leftView | rightView | * | | | * | | | * | | | * | | | * | | | * | | | * | | | * +----------+--------------------------+ */ var HorizontalSplitView = (function() { 'use strict'; // We inherit from View. var superClass = View; /** * @param {!View} leftView The left view. * @param {!View} rightView The right view. * @constructor */ function HorizontalSplitView(leftView, rightView) { // Call superclass's constructor. superClass.call(this); this.leftView_ = leftView; this.rightView_ = rightView; } HorizontalSplitView.prototype = { // Inherit the superclass's methods. __proto__: superClass.prototype, setGeometry: function(left, top, width, height) { superClass.prototype.setGeometry.call(this, left, top, width, height); var fixedWidth = this.leftView_.getWidth(); this.leftView_.setGeometry(left, top, fixedWidth, height); this.rightView_.setGeometry( left + fixedWidth, top, width - fixedWidth, height); }, show: function(isVisible) { superClass.prototype.show.call(this, isVisible); this.leftView_.show(isVisible); this.rightView_.show(isVisible); } }; return HorizontalSplitView; })(); // Copyright (c) 2012 The Chromium Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. /** * Class to handle display and placement of a div that appears under the cursor * when it overs over a specied element. The div always appears below and to * the left of the cursor. */ var MouseOverHelp = (function() { 'use strict'; /** * @param {string} helpDivId Name of the div to position and display * @param {string} mouseOverElementId Name the element that displays the * |helpDivId| div on mouse over. * @constructor */ function MouseOverHelp(helpDivId, mouseOverElementId) { this.node_ = $(helpDivId); $(mouseOverElementId).onmouseover = this.onMouseOver.bind(this); $(mouseOverElementId).onmouseout = this.onMouseOut.bind(this); this.show(false); } MouseOverHelp.prototype = { /** * Positions and displays the div, if not already visible. * @param {MouseEvent} event Mouse event that triggered the call. */ onMouseOver: function(event) { if (this.isVisible_) return; this.node_.style.position = 'absolute'; this.show(true); this.node_.style.left = (event.clientX + 15).toFixed(0) + 'px'; this.node_.style.top = event.clientY.toFixed(0) + 'px'; }, /** * Hides the div when the cursor leaves the hover element. * @param {MouseEvent} event Mouse event that triggered the call. */ onMouseOut: function(event) { this.show(false); }, /** * Sets the div's visibility. * @param {boolean} isVisible True if the help div should be shown. */ show: function(isVisible) { setNodeDisplay(this.node_, isVisible); this.isVisible_ = isVisible; }, }; return MouseOverHelp; })(); // Copyright (c) 2012 The Chromium Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. var TabSwitcherView = (function() { 'use strict'; // We inherit from View. var superClass = View; /** * TabSwitcherView is an implementation of View that handles tab switching. * * It is comprised of many views (tabs), only one of which is visible at a * time. * * This view represents solely the selected tab's content area -- a separate * view needs to be maintained for the tab handles. * * @constructor */ function TabSwitcherView() { superClass.call(this); this.tabs_ = []; } TabSwitcherView.prototype = { // Inherit the superclass's methods. __proto__: superClass.prototype, setGeometry: function(left, top, width, height) { superClass.prototype.setGeometry.call(this, left, top, width, height); // Position each of the tabs content areas. for (var i = 0; i < this.tabs_.length; ++i) { var tab = this.tabs_[i]; tab.contentView.setGeometry(left, top, width, height); } }, show: function(isVisible) { superClass.prototype.show.call(this, isVisible); var activeTab = this.findActiveTab(); if (activeTab) activeTab.contentView.show(isVisible); }, /** * Adds a new tab (initially hidden). * * @param {String} id The ID for DOM node that will be made clickable to * select this tab. This is also the ID we use to * identify the "tab". * @param {!View} view The tab's actual contents. */ addTab: function(id, contentView, switchOnClick, visible) { var tab = new TabEntry(id, contentView); this.tabs_.push(tab); if (switchOnClick) { // Attach a click handler, used to switch to the tab. var self = this; tab.getTabHandleNode().onclick = function() { self.switchToTab(id, null); }; } // Start tabs off as hidden. tab.contentView.show(false); this.showTabHandleNode(id, visible); }, /** * @return {?TabEntry} The currently selected tab, or null if there is none. */ findActiveTab: function() { for (var i = 0; i < this.tabs_.length; ++i) { var tab = this.tabs_[i]; if (tab.active) return tab; } return null; }, /** * @return {?TabEntry} The tab with ID |id|, or null if there is none. */ findTabById: function(id) { for (var i = 0; i < this.tabs_.length; ++i) { var tab = this.tabs_[i]; if (tab.id == id) return tab; } return null; }, /** * Focuses on tab with ID |id|. |params| is a dictionary that will be * passed to the tab's setParameters function, if it's non-null. */ switchToTab: function(id, params) { var oldTab = this.findActiveTab(); if (oldTab) oldTab.setSelected(false); var newTab = this.findTabById(id); newTab.setSelected(true); if (params) newTab.contentView.setParameters(params); // Update data needed by newly active tab, as it may be // significantly out of date. if (typeof g_browser != 'undefined' && g_browser.checkForUpdatedInfo) g_browser.checkForUpdatedInfo(); }, getAllTabIds: function() { var ids = []; for (var i = 0; i < this.tabs_.length; ++i) ids.push(this.tabs_[i].id); return ids; }, /** * Shows/hides the DOM node that is used to select the tab. If the * specified tab is the active tab, switches the active tab to the first * still visible tab in the tab list. */ showTabHandleNode: function(id, isVisible) { var tab = this.findTabById(id); if (!isVisible && tab == this.findActiveTab()) { for (var i = 0; i < this.tabs_.length; ++i) { if (this.tabs_[i].id != id && this.tabs_[i].getTabHandleNode().style.display != 'none') { this.switchToTab(this.tabs_[i].id, null); break; } } } setNodeDisplay(tab.getTabHandleNode(), isVisible); } }; //--------------------------------------------------------------------------- /** * @constructor */ function TabEntry(id, contentView) { this.id = id; this.contentView = contentView; } TabEntry.prototype.setSelected = function(isSelected) { this.active = isSelected; if (isSelected) this.getTabHandleNode().classList.add('selected'); else this.getTabHandleNode().classList.remove('selected'); this.contentView.show(isSelected); }; /** * Returns the DOM node that is used to select the tab. */ TabEntry.prototype.getTabHandleNode = function() { return $(this.id); }; return TabSwitcherView; })(); // Copyright (c) 2012 The Chromium Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. /** * This view displays options for importing data from a log file. */ var ImportView = (function() { 'use strict'; // This is defined in index.html, but for all intents and purposes is part // of this view. var LOAD_LOG_FILE_DROP_TARGET_ID = 'import-view-drop-target'; // We inherit from DivView. var superClass = DivView; /** * @constructor */ function ImportView() { assertFirstConstructorCall(ImportView); // Call superclass's constructor. superClass.call(this, ImportView.MAIN_BOX_ID); this.loadedDiv_ = $(ImportView.LOADED_DIV_ID); this.loadFileElement_ = $(ImportView.LOAD_LOG_FILE_ID); this.loadFileElement_.onchange = this.logFileChanged.bind(this); this.loadStatusText_ = $(ImportView.LOAD_STATUS_TEXT_ID); var dropTarget = $(LOAD_LOG_FILE_DROP_TARGET_ID); dropTarget.ondragenter = this.onDrag.bind(this); dropTarget.ondragover = this.onDrag.bind(this); dropTarget.ondrop = this.onDrop.bind(this); this.loadedInfoBuildName_ = $(ImportView.LOADED_INFO_BUILD_NAME_ID); this.loadedInfoExportDate_ = $(ImportView.LOADED_INFO_EXPORT_DATE_ID); this.loadedInfoOsType_ = $(ImportView.LOADED_INFO_OS_TYPE_ID); this.loadedInfoCommandLine_ = $(ImportView.LOADED_INFO_COMMAND_LINE_ID); this.loadedInfoUserComments_ = $(ImportView.LOADED_INFO_USER_COMMENTS_ID); } ImportView.TAB_HANDLE_ID = 'tab-handle-import'; // IDs for special HTML elements in import_view.html ImportView.MAIN_BOX_ID = 'import-view-tab-content'; ImportView.LOADED_DIV_ID = 'import-view-loaded-div'; ImportView.LOAD_LOG_FILE_ID = 'import-view-load-log-file'; ImportView.LOAD_STATUS_TEXT_ID = 'import-view-load-status-text'; ImportView.LOADED_INFO_EXPORT_DATE_ID = 'import-view-export-date'; ImportView.LOADED_INFO_BUILD_NAME_ID = 'import-view-build-name'; ImportView.LOADED_INFO_OS_TYPE_ID = 'import-view-os-type'; ImportView.LOADED_INFO_COMMAND_LINE_ID = 'import-view-command-line'; ImportView.LOADED_INFO_USER_COMMENTS_ID = 'import-view-user-comments'; cr.addSingletonGetter(ImportView); ImportView.prototype = { // Inherit the superclass's methods. __proto__: superClass.prototype, /** * Called when a log file is loaded, after clearing the old log entries and * loading the new ones. Returns true to indicate the view should * still be visible. */ onLoadLogFinish: function(data, unused, logDump) { setNodeDisplay(this.loadedDiv_, true); this.updateLoadedClientInfo(logDump.userComments); return true; }, /** * Called when something is dragged over the drop target. * * Returns false to cancel default browser behavior when a single file is * being dragged. When this happens, we may not receive a list of files for * security reasons, which is why we allow the |files| array to be empty. */ onDrag: function(event) { // NOTE: Use Array.prototype.indexOf here is necessary while WebKit // decides which type of data structure dataTransfer.types will be // (currently between DOMStringList and Array). These have different APIs // so assuming one type or the other was breaking things. See // http://crbug.com/115433. TODO(dbeam): Remove when standardized more. var indexOf = Array.prototype.indexOf; return indexOf.call(event.dataTransfer.types, 'Files') == -1 || event.dataTransfer.files.length > 1; }, /** * Called when something is dropped onto the drop target. If it's a single * file, tries to load it as a log file. */ onDrop: function(event) { var indexOf = Array.prototype.indexOf; if (indexOf.call(event.dataTransfer.types, 'Files') == -1 || event.dataTransfer.files.length != 1) { return; } event.preventDefault(); // Loading a log file may hide the currently active tab. Switch to the // import tab to prevent this. document.location.hash = 'import'; this.loadLogFile(event.dataTransfer.files[0]); }, /** * Called when a log file is selected. * * Gets the log file from the input element and tries to read from it. */ logFileChanged: function() { this.loadLogFile(this.loadFileElement_.files[0]); }, /** * Attempts to read from the File |logFile|. */ loadLogFile: function(logFile) { if (logFile) { this.setLoadFileStatus('Loading log...', true); var fileReader = new FileReader(); fileReader.onload = this.onLoadLogFile.bind(this, logFile); fileReader.onerror = this.onLoadLogFileError.bind(this); fileReader.readAsText(logFile); } }, /** * Displays an error message when unable to read the selected log file. * Also clears the file input control, so the same file can be reloaded. */ onLoadLogFileError: function(event) { this.loadFileElement_.value = null; this.setLoadFileStatus( 'Error ' + getKeyWithValue(FileError, event.target.error.code) + '. Unable to read file.', false); }, onLoadLogFile: function(logFile, event) { var result = log_util.loadLogFile(event.target.result, logFile.name); this.setLoadFileStatus(result, false); }, /** * Sets the load from file status text, displayed below the load file * button, to |text|. Also enables or disables the load buttons based on * the value of |isLoading|, which must be true if the load process is still * ongoing, and false when the operation has stopped, regardless of success * of failure. Also, when loading is done, replaces the load button so the * same file can be loaded again. */ setLoadFileStatus: function(text, isLoading) { this.enableLoadFileElement_(!isLoading); this.loadStatusText_.textContent = text; if (!isLoading) { // Clear the button, so the same file can be reloaded. Recreating the // element seems to be the only way to do this. var loadFileElementId = this.loadFileElement_.id; var loadFileElementOnChange = this.loadFileElement_.onchange; this.loadFileElement_.outerHTML = this.loadFileElement_.outerHTML; this.loadFileElement_ = $(loadFileElementId); this.loadFileElement_.onchange = loadFileElementOnChange; } // Style the log output differently depending on what just happened. var pos = text.indexOf('Log loaded.'); if (isLoading) { this.loadStatusText_.className = 'import-view-pending-log'; } else if (pos == 0) { this.loadStatusText_.className = 'import-view-success-log'; } else if (pos != -1) { this.loadStatusText_.className = 'import-view-warning-log'; } else { this.loadStatusText_.className = 'import-view-error-log'; } }, enableLoadFileElement_: function(enabled) { this.loadFileElement_.disabled = !enabled; }, /** * Prints some basic information about the environment when the log was * made. */ updateLoadedClientInfo: function(userComments) { // Reset all the fields (in case we early-return). this.loadedInfoExportDate_.innerText = ''; this.loadedInfoBuildName_.innerText = ''; this.loadedInfoOsType_.innerText = ''; this.loadedInfoCommandLine_.innerText = ''; this.loadedInfoUserComments_.innerText = ''; if (typeof(ClientInfo) != 'object') return; timeutil.addNodeWithDate(this.loadedInfoExportDate_, new Date(ClientInfo.numericDate)); var buildName = ClientInfo.name + ' ' + ClientInfo.version + ' (' + ClientInfo.official + ' ' + ClientInfo.cl + ') ' + ClientInfo.version_mod; this.loadedInfoBuildName_.innerText = buildName; this.loadedInfoOsType_.innerText = ClientInfo.os_type; this.loadedInfoCommandLine_.innerText = ClientInfo.command_line; // User comments will not be available when dumped from command line. this.loadedInfoUserComments_.innerText = userComments || ''; } }; return ImportView; })(); // Copyright (c) 2012 The Chromium Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. /** * This view displays controls for capturing network events. */ var CaptureView = (function() { 'use strict'; // We inherit from DivView. var superClass = DivView; /** * @constructor */ function CaptureView() { assertFirstConstructorCall(CaptureView); // Call superclass's constructor. superClass.call(this, CaptureView.MAIN_BOX_ID); var byteLoggingCheckbox = $(CaptureView.BYTE_LOGGING_CHECKBOX_ID); byteLoggingCheckbox.onclick = this.onSetByteLogging_.bind(this); $(CaptureView.LIMIT_CHECKBOX_ID).onclick = this.onChangeLimit_.bind(this); $(CaptureView.TIP_ANCHOR_ID).onclick = this.toggleCommandLineTip_.bind(this, CaptureView.TIP_DIV_ID); if (byteLoggingCheckbox.checked) { // The code to display a warning on ExportView relies on bytelogging // being off by default. If this ever changes, the code will need to // be updated. throw 'Not expecting byte logging to be enabled!'; } this.onChangeLimit_(); } // ID for special HTML element in category_tabs.html CaptureView.TAB_HANDLE_ID = 'tab-handle-capture'; // IDs for special HTML elements in capture_view.html CaptureView.MAIN_BOX_ID = 'capture-view-tab-content'; CaptureView.BYTE_LOGGING_CHECKBOX_ID = 'capture-view-byte-logging-checkbox'; CaptureView.LIMIT_CHECKBOX_ID = 'capture-view-limit-checkbox'; CaptureView.TIP_ANCHOR_ID = 'capture-view-tip-anchor'; CaptureView.TIP_DIV_ID = 'capture-view-tip-div'; cr.addSingletonGetter(CaptureView); CaptureView.prototype = { // Inherit the superclass's methods. __proto__: superClass.prototype, /** * Toggles the visilibity on the command-line tip. */ toggleCommandLineTip_: function(divId) { var n = $(divId); var isVisible = n.style.display != 'none'; setNodeDisplay(n, !isVisible); return false; // Prevent default handling of the click. }, /** * Called when a log file is loaded, after clearing the old log entries and * loading the new ones. Returns false to indicate the view should * be hidden. */ onLoadLogFinish: function(data) { return false; }, /** * Depending on the value of the checkbox, enables or disables logging of * actual bytes transferred. */ onSetByteLogging_: function() { var byteLoggingCheckbox = $(CaptureView.BYTE_LOGGING_CHECKBOX_ID); if (byteLoggingCheckbox.checked) { g_browser.setLogLevel(LogLevelType.LOG_ALL); // Once we enable byte logging, all bets are off on what gets captured. // Have the export view warn that the "strip cookies" option is // ineffective from this point on. // // In theory we could clear this warning after unchecking the box and // then deleting all the events which had been captured. We don't // currently do that; if you want the warning to go away, will need to // reload. ExportView.getInstance().showPrivacyWarning(); } else { g_browser.setLogLevel(LogLevelType.LOG_ALL_BUT_BYTES); } }, onChangeLimit_: function() { var limitCheckbox = $(CaptureView.LIMIT_CHECKBOX_ID); // Default to unlimited. var softLimit = Infinity; var hardLimit = Infinity; if (limitCheckbox.checked) { // The chosen limits are kind of arbitrary. I based it off the // following observation: // A user-submitted log file which spanned a 7 hour time period // comprised 778,235 events and required 128MB of JSON. // // That feels too big. Assuming it was representative, then scaling // by a factor of 4 should translate into a 32MB log file and cover // close to 2 hours of events, which feels better. // // A large gap is left between the hardLimit and softLimit to avoid // resetting the events often. hardLimit = 300000; softLimit = 150000; } EventsTracker.getInstance().setLimits(softLimit, hardLimit); }, }; return CaptureView; })(); // Copyright (c) 2012 The Chromium Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. /** * This view displays options for exporting the captured data. */ var ExportView = (function() { 'use strict'; // We inherit from DivView. var superClass = DivView; /** * @constructor */ function ExportView() { assertFirstConstructorCall(ExportView); // Call superclass's constructor. superClass.call(this, ExportView.MAIN_BOX_ID); var privacyStrippingCheckbox = $(ExportView.PRIVACY_STRIPPING_CHECKBOX_ID); privacyStrippingCheckbox.onclick = this.onSetPrivacyStripping_.bind(this, privacyStrippingCheckbox); this.saveFileButton_ = $(ExportView.SAVE_FILE_BUTTON_ID); this.saveFileButton_.onclick = this.onSaveFile_.bind(this); this.saveStatusText_ = $(ExportView.SAVE_STATUS_TEXT_ID); this.userCommentsTextArea_ = $(ExportView.USER_COMMENTS_TEXT_AREA_ID); // Track blob for previous log dump so it can be revoked when a new dump is // saved. this.lastBlobURL_ = null; // Cached copy of the last loaded log dump, for use when exporting. this.loadedLogDump_ = null; } // ID for special HTML element in category_tabs.html ExportView.TAB_HANDLE_ID = 'tab-handle-export'; // IDs for special HTML elements in export_view.html ExportView.MAIN_BOX_ID = 'export-view-tab-content'; ExportView.DOWNLOAD_ANCHOR_ID = 'export-view-download-anchor'; ExportView.SAVE_FILE_BUTTON_ID = 'export-view-save-log-file'; ExportView.SAVE_STATUS_TEXT_ID = 'export-view-save-status-text'; ExportView.PRIVACY_STRIPPING_CHECKBOX_ID = 'export-view-privacy-stripping-checkbox'; ExportView.USER_COMMENTS_TEXT_AREA_ID = 'export-view-user-comments'; ExportView.PRIVACY_WARNING_ID = 'export-view-privacy-warning'; cr.addSingletonGetter(ExportView); ExportView.prototype = { // Inherit the superclass's methods. __proto__: superClass.prototype, /** * Depending on the value of the checkbox, enables or disables stripping * cookies and passwords from log dumps and displayed events. */ onSetPrivacyStripping_: function(privacyStrippingCheckbox) { SourceTracker.getInstance().setPrivacyStripping( privacyStrippingCheckbox.checked); }, /** * When loading a log dump, cache it for future export and continue showing * the ExportView. */ onLoadLogFinish: function(polledData, tabData, logDump) { this.loadedLogDump_ = logDump; this.setUserComments_(logDump.userComments); return true; }, /** * Sets the save to file status text, displayed below the save to file * button, to |text|. Also enables or disables the save button based on the * value of |isSaving|, which must be true if the save process is still * ongoing, and false when the operation has stopped, regardless of success * of failure. */ setSaveFileStatus: function(text, isSaving) { this.enableSaveFileButton_(!isSaving); this.saveStatusText_.textContent = text; }, enableSaveFileButton_: function(enabled) { this.saveFileButton_.disabled = !enabled; }, showPrivacyWarning: function() { setNodeDisplay($(ExportView.PRIVACY_WARNING_ID), true); $(ExportView.PRIVACY_STRIPPING_CHECKBOX_ID).checked = false; $(ExportView.PRIVACY_STRIPPING_CHECKBOX_ID).disabled = true; // Updating the checkbox doesn't actually disable privacy stripping, since // the onclick function will not be called. this.onSetPrivacyStripping_($(ExportView.PRIVACY_STRIPPING_CHECKBOX_ID)); }, /** * If not already busy saving a log dump, triggers asynchronous * generation of log dump and starts waiting for it to complete. */ onSaveFile_: function() { if (this.saveFileButton_.disabled) return; // Clean up previous blob, if any, to reduce resource usage. if (this.lastBlobURL_) { window.webkitURL.revokeObjectURL(this.lastBlobURL_); this.lastBlobURL_ = null; } this.createLogDump_(this.onLogDumpCreated_.bind(this)); }, /** * Creates a log dump, and either synchronously or asynchronously calls * |callback| if it succeeds. Separate from onSaveFile_ for unit tests. */ createLogDump_: function(callback) { // Get an explanation for the dump file (this is mandatory!) var userComments = this.getNonEmptyUserComments_(); if (userComments == undefined) { return; } this.setSaveFileStatus('Preparing data...', true); var privacyStripping = SourceTracker.getInstance().getPrivacyStripping(); // If we have a cached log dump, update it synchronously. if (this.loadedLogDump_) { var dumpText = log_util.createUpdatedLogDump(userComments, this.loadedLogDump_, privacyStripping); callback(dumpText); return; } // Otherwise, poll information from the browser before creating one. log_util.createLogDumpAsync(userComments, callback, privacyStripping); }, /** * Sets the user comments. */ setUserComments_: function(userComments) { this.userCommentsTextArea_.value = userComments; }, /** * Fetches the user comments for this dump. If none were entered, warns the * user and returns undefined. Otherwise returns the comments text. */ getNonEmptyUserComments_: function() { var value = this.userCommentsTextArea_.value; // Reset the class name in case we had hilighted it earlier. this.userCommentsTextArea_.className = ''; // We don't accept empty explanations. We don't care what is entered, as // long as there is something (a single whitespace would work). if (value == '') { // Put a big obnoxious red border around the text area. this.userCommentsTextArea_.className = 'export-view-explanation-warning'; alert('Please fill in the text field!'); return undefined; } return value; }, /** * Creates a blob url and starts downloading it. */ onLogDumpCreated_: function(dumpText) { var textBlob = new Blob([dumpText], {type: 'octet/stream'}); this.lastBlobURL_ = window.webkitURL.createObjectURL(textBlob); // Update the anchor tag and simulate a click on it to start the // download. var downloadAnchor = $(ExportView.DOWNLOAD_ANCHOR_ID); downloadAnchor.href = this.lastBlobURL_; downloadAnchor.click(); this.setSaveFileStatus('Dump successful', false); } }; return ExportView; })(); // Copyright (c) 2011 The Chromium Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. /** * This view displays information on the HTTP cache. */ var HttpCacheView = (function() { 'use strict'; // We inherit from DivView. var superClass = DivView; /** * @constructor */ function HttpCacheView() { assertFirstConstructorCall(HttpCacheView); // Call superclass's constructor. superClass.call(this, HttpCacheView.MAIN_BOX_ID); this.statsDiv_ = $(HttpCacheView.STATS_DIV_ID); // Register to receive http cache info. g_browser.addHttpCacheInfoObserver(this, true); } // ID for special HTML element in category_tabs.html HttpCacheView.TAB_HANDLE_ID = 'tab-handle-http-cache'; // IDs for special HTML elements in http_cache_view.html HttpCacheView.MAIN_BOX_ID = 'http-cache-view-tab-content'; HttpCacheView.STATS_DIV_ID = 'http-cache-view-cache-stats'; cr.addSingletonGetter(HttpCacheView); HttpCacheView.prototype = { // Inherit the superclass's methods. __proto__: superClass.prototype, onLoadLogFinish: function(data) { return this.onHttpCacheInfoChanged(data.httpCacheInfo); }, onHttpCacheInfoChanged: function(info) { this.statsDiv_.innerHTML = ''; if (!info) return false; // Print the statistics. var statsUl = addNode(this.statsDiv_, 'ul'); for (var statName in info.stats) { var li = addNode(statsUl, 'li'); addTextNode(li, statName + ': ' + info.stats[statName]); } return true; } }; return HttpCacheView; })(); // Copyright (c) 2012 The Chromium Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. /** * This view displays the progress and results from the "connection tester". * * - Has an input box to specify the URL. * - Has a button to start running the tests. * - Shows the set of experiments that have been run so far, and their * result. */ var TestView = (function() { 'use strict'; // We inherit from DivView. var superClass = DivView; /** * @constructor */ function TestView() { assertFirstConstructorCall(TestView); // Call superclass's constructor. superClass.call(this, TestView.MAIN_BOX_ID); this.urlInput_ = $(TestView.URL_INPUT_ID); this.summaryDiv_ = $(TestView.SUMMARY_DIV_ID); var form = $(TestView.FORM_ID); form.addEventListener('submit', this.onSubmitForm_.bind(this), false); // Register to test information as it's received. g_browser.addConnectionTestsObserver(this); } // ID for special HTML element in category_tabs.html TestView.TAB_HANDLE_ID = 'tab-handle-tests'; // IDs for special HTML elements in test_view.html TestView.MAIN_BOX_ID = 'test-view-tab-content'; TestView.FORM_ID = 'test-view-connection-tests-form'; TestView.URL_INPUT_ID = 'test-view-url-input'; TestView.SUMMARY_DIV_ID = 'test-view-summary'; // Needed by tests. TestView.SUBMIT_BUTTON_ID = 'test-view-connection-tests-submit'; cr.addSingletonGetter(TestView); TestView.prototype = { // Inherit the superclass's methods. __proto__: superClass.prototype, onSubmitForm_: function(event) { g_browser.sendStartConnectionTests(this.urlInput_.value); event.preventDefault(); }, /** * Callback for when the connection tests have begun. */ onStartedConnectionTestSuite: function() { this.summaryDiv_.innerHTML = ''; var p = addNode(this.summaryDiv_, 'p'); addTextNode(p, 'Started connection test suite suite on '); timeutil.addNodeWithDate(p, new Date()); // Add a table that will hold the individual test results. var table = addNode(this.summaryDiv_, 'table'); table.className = 'styled-table'; var thead = addNode(table, 'thead'); thead.innerHTML = 'ResultExperiment' + 'ErrorTime (ms)'; this.tbody_ = addNode(table, 'tbody'); }, /** * Callback for when an individual test in the suite has begun. */ onStartedConnectionTestExperiment: function(experiment) { var tr = addNode(this.tbody_, 'tr'); var passFailCell = addNode(tr, 'td'); var experimentCell = addNode(tr, 'td'); var resultCell = addNode(tr, 'td'); addTextNode(resultCell, '?'); var dtCell = addNode(tr, 'td'); addTextNode(dtCell, '?'); // We will fill in result cells with actual values (to replace the // placeholder '?') once the test has completed. For now we just // save references to these cells. this.currentExperimentRow_ = { experimentCell: experimentCell, dtCell: dtCell, resultCell: resultCell, passFailCell: passFailCell, startTime: timeutil.getCurrentTime() }; addTextNode(experimentCell, 'Fetch ' + experiment.url); if (experiment.proxy_settings_experiment || experiment.host_resolver_experiment) { var ul = addNode(experimentCell, 'ul'); if (experiment.proxy_settings_experiment) { var li = addNode(ul, 'li'); addTextNode(li, experiment.proxy_settings_experiment); } if (experiment.host_resolver_experiment) { var li = addNode(ul, 'li'); addTextNode(li, experiment.host_resolver_experiment); } } }, /** * Callback for when an individual test in the suite has finished. */ onCompletedConnectionTestExperiment: function(experiment, result) { var r = this.currentExperimentRow_; var endTime = timeutil.getCurrentTime(); r.dtCell.innerHTML = ''; addTextNode(r.dtCell, (endTime - r.startTime)); r.resultCell.innerHTML = ''; if (result == 0) { r.passFailCell.style.color = 'green'; addTextNode(r.passFailCell, 'PASS'); } else { addTextNode(r.resultCell, netErrorToString(result) + ' (' + result + ')'); r.passFailCell.style.color = 'red'; addTextNode(r.passFailCell, 'FAIL'); } this.currentExperimentRow_ = null; }, /** * Callback for when the last test in the suite has finished. */ onCompletedConnectionTestSuite: function() { var p = addNode(this.summaryDiv_, 'p'); addTextNode(p, 'Completed connection test suite suite'); } }; return TestView; })(); // Copyright (c) 2012 The Chromium Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. /** * HSTS is HTTPS Strict Transport Security: a way for sites to elect to always * use HTTPS. See http://dev.chromium.org/sts * * This UI allows a user to query and update the browser's list of HSTS domains. * It also allows users to query and update the browser's list of public key * pins. */ var HSTSView = (function() { 'use strict'; // We inherit from DivView. var superClass = DivView; /** * @constructor */ function HSTSView() { assertFirstConstructorCall(HSTSView); // Call superclass's constructor. superClass.call(this, HSTSView.MAIN_BOX_ID); this.addInput_ = $(HSTSView.ADD_INPUT_ID); this.addCheck_ = $(HSTSView.ADD_CHECK_ID); this.addPins_ = $(HSTSView.ADD_PINS_ID); this.deleteInput_ = $(HSTSView.DELETE_INPUT_ID); this.queryInput_ = $(HSTSView.QUERY_INPUT_ID); this.queryOutputDiv_ = $(HSTSView.QUERY_OUTPUT_DIV_ID); var form = $(HSTSView.ADD_FORM_ID); form.addEventListener('submit', this.onSubmitAdd_.bind(this), false); form = $(HSTSView.DELETE_FORM_ID); form.addEventListener('submit', this.onSubmitDelete_.bind(this), false); form = $(HSTSView.QUERY_FORM_ID); form.addEventListener('submit', this.onSubmitQuery_.bind(this), false); g_browser.addHSTSObserver(this); } // ID for special HTML element in category_tabs.html HSTSView.TAB_HANDLE_ID = 'tab-handle-hsts'; // IDs for special HTML elements in hsts_view.html HSTSView.MAIN_BOX_ID = 'hsts-view-tab-content'; HSTSView.ADD_INPUT_ID = 'hsts-view-add-input'; HSTSView.ADD_CHECK_ID = 'hsts-view-check-input'; HSTSView.ADD_PINS_ID = 'hsts-view-add-pins'; HSTSView.ADD_FORM_ID = 'hsts-view-add-form'; HSTSView.ADD_SUBMIT_ID = 'hsts-view-add-submit'; HSTSView.DELETE_INPUT_ID = 'hsts-view-delete-input'; HSTSView.DELETE_FORM_ID = 'hsts-view-delete-form'; HSTSView.DELETE_SUBMIT_ID = 'hsts-view-delete-submit'; HSTSView.QUERY_INPUT_ID = 'hsts-view-query-input'; HSTSView.QUERY_OUTPUT_DIV_ID = 'hsts-view-query-output'; HSTSView.QUERY_FORM_ID = 'hsts-view-query-form'; HSTSView.QUERY_SUBMIT_ID = 'hsts-view-query-submit'; cr.addSingletonGetter(HSTSView); HSTSView.prototype = { // Inherit the superclass's methods. __proto__: superClass.prototype, onSubmitAdd_: function(event) { g_browser.sendHSTSAdd(this.addInput_.value, this.addCheck_.checked, this.addPins_.value); g_browser.sendHSTSQuery(this.addInput_.value); this.queryInput_.value = this.addInput_.value; this.addCheck_.checked = false; this.addInput_.value = ''; this.addPins_.value = ''; event.preventDefault(); }, onSubmitDelete_: function(event) { g_browser.sendHSTSDelete(this.deleteInput_.value); this.deleteInput_.value = ''; event.preventDefault(); }, onSubmitQuery_: function(event) { g_browser.sendHSTSQuery(this.queryInput_.value); event.preventDefault(); }, onHSTSQueryResult: function(result) { if (result.error != undefined) { this.queryOutputDiv_.innerHTML = ''; var s = addNode(this.queryOutputDiv_, 'span'); s.textContent = result.error; s.style.color = 'red'; yellowFade(this.queryOutputDiv_); return; } if (result.result == false) { this.queryOutputDiv_.innerHTML = 'Not found'; yellowFade(this.queryOutputDiv_); return; } this.queryOutputDiv_.innerHTML = ''; var s = addNode(this.queryOutputDiv_, 'span'); s.innerHTML = 'Found: mode: '; var t = addNode(this.queryOutputDiv_, 'tt'); t.textContent = modeToString(result.mode); addTextNode(this.queryOutputDiv_, ' include_subdomains:'); t = addNode(this.queryOutputDiv_, 'tt'); t.textContent = result.subdomains; addTextNode(this.queryOutputDiv_, ' domain:'); t = addNode(this.queryOutputDiv_, 'tt'); t.textContent = result.domain; addTextNode(this.queryOutputDiv_, ' pubkey_hashes:'); t = addNode(this.queryOutputDiv_, 'tt'); // |public_key_hashes| is an old synonym for what is now // |preloaded_spki_hashes|, which in turn is a legacy synonym for // |static_spki_hashes|. Look for all three, and also for // |dynamic_spki_hashes|. if (typeof result.public_key_hashes === 'undefined') result.public_key_hashes = ''; if (typeof result.preloaded_spki_hashes === 'undefined') result.preloaded_spki_hashes = ''; if (typeof result.static_spki_hashes === 'undefined') result.static_spki_hashes = ''; if (typeof result.dynamic_spki_hashes === 'undefined') result.dynamic_spki_hashes = ''; var hashes = []; if (result.public_key_hashes) hashes.push(result.public_key_hashes); if (result.preloaded_spki_hashes) hashes.push(result.preloaded_spki_hashes); if (result.static_spki_hashes) hashes.push(result.static_spki_hashes); if (result.dynamic_spki_hashes) hashes.push(result.dynamic_spki_hashes); t.textContent = hashes.join(','); yellowFade(this.queryOutputDiv_); } }; function modeToString(m) { // These numbers must match those in // TransportSecurityState::DomainState::UpgradeMode. if (m == 0) { return 'STRICT'; } else if (m == 1) { return 'OPPORTUNISTIC'; } else { return 'UNKNOWN'; } } function yellowFade(element) { element.style.webkitTransitionProperty = 'background-color'; element.style.webkitTransitionDuration = '0'; element.style.backgroundColor = '#fffccf'; setTimeout(function() { element.style.webkitTransitionDuration = '1000ms'; element.style.backgroundColor = '#fff'; }, 0); } return HSTSView; })(); // Copyright (c) 2012 The Chromium Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. /** * This class provides a "bridge" for communicating between the javascript and * the browser. */ var BrowserBridge = (function() { 'use strict'; /** * Delay in milliseconds between updates of certain browser information. */ var POLL_INTERVAL_MS = 5000; /** * @constructor */ function BrowserBridge() { assertFirstConstructorCall(BrowserBridge); // List of observers for various bits of browser state. this.connectionTestsObservers_ = []; this.hstsObservers_ = []; this.constantsObservers_ = []; this.crosONCFileParseObservers_ = []; this.storeDebugLogsObservers_ = []; this.setNetworkDebugModeObservers_ = []; // Unprocessed data received before the constants. This serves to protect // against passing along data before having information on how to interpret // it. this.earlyReceivedData_ = []; this.pollableDataHelpers_ = {}; this.pollableDataHelpers_.proxySettings = new PollableDataHelper('onProxySettingsChanged', this.sendGetProxySettings.bind(this)); this.pollableDataHelpers_.badProxies = new PollableDataHelper('onBadProxiesChanged', this.sendGetBadProxies.bind(this)); this.pollableDataHelpers_.httpCacheInfo = new PollableDataHelper('onHttpCacheInfoChanged', this.sendGetHttpCacheInfo.bind(this)); this.pollableDataHelpers_.hostResolverInfo = new PollableDataHelper('onHostResolverInfoChanged', this.sendGetHostResolverInfo.bind(this)); this.pollableDataHelpers_.socketPoolInfo = new PollableDataHelper('onSocketPoolInfoChanged', this.sendGetSocketPoolInfo.bind(this)); this.pollableDataHelpers_.sessionNetworkStats = new PollableDataHelper('onSessionNetworkStatsChanged', this.sendGetSessionNetworkStats.bind(this)); this.pollableDataHelpers_.historicNetworkStats = new PollableDataHelper('onHistoricNetworkStatsChanged', this.sendGetHistoricNetworkStats.bind(this)); this.pollableDataHelpers_.spdySessionInfo = new PollableDataHelper('onSpdySessionInfoChanged', this.sendGetSpdySessionInfo.bind(this)); this.pollableDataHelpers_.spdyStatus = new PollableDataHelper('onSpdyStatusChanged', this.sendGetSpdyStatus.bind(this)); this.pollableDataHelpers_.spdyAlternateProtocolMappings = new PollableDataHelper('onSpdyAlternateProtocolMappingsChanged', this.sendGetSpdyAlternateProtocolMappings.bind( this)); if (cr.isWindows) { this.pollableDataHelpers_.serviceProviders = new PollableDataHelper('onServiceProvidersChanged', this.sendGetServiceProviders.bind(this)); } this.pollableDataHelpers_.prerenderInfo = new PollableDataHelper('onPrerenderInfoChanged', this.sendGetPrerenderInfo.bind(this)); this.pollableDataHelpers_.httpPipeliningStatus = new PollableDataHelper('onHttpPipeliningStatusChanged', this.sendGetHttpPipeliningStatus.bind(this)); // Setting this to true will cause messages from the browser to be ignored, // and no messages will be sent to the browser, either. Intended for use // when viewing log files. this.disabled_ = false; // Interval id returned by window.setInterval for polling timer. this.pollIntervalId_ = null; } cr.addSingletonGetter(BrowserBridge); BrowserBridge.prototype = { //-------------------------------------------------------------------------- // Messages sent to the browser //-------------------------------------------------------------------------- /** * Wraps |chrome.send|. Doesn't send anything when disabled. */ send: function(value1, value2) { if (!this.disabled_) { if (arguments.length == 1) { chrome.send(value1); } else if (arguments.length == 2) { chrome.send(value1, value2); } else { throw 'Unsupported number of arguments.'; } } }, sendReady: function() { this.send('notifyReady'); this.setPollInterval(POLL_INTERVAL_MS); }, /** * Some of the data we are interested is not currently exposed as a * stream. This starts polling those with active observers (visible * views) every |intervalMs|. Subsequent calls override previous calls * to this function. If |intervalMs| is 0, stops polling. */ setPollInterval: function(intervalMs) { if (this.pollIntervalId_ !== null) { window.clearInterval(this.pollIntervalId_); this.pollIntervalId_ = null; } if (intervalMs > 0) { this.pollIntervalId_ = window.setInterval(this.checkForUpdatedInfo.bind(this, false), intervalMs); } }, sendGetProxySettings: function() { // The browser will call receivedProxySettings on completion. this.send('getProxySettings'); }, sendReloadProxySettings: function() { this.send('reloadProxySettings'); }, sendGetBadProxies: function() { // The browser will call receivedBadProxies on completion. this.send('getBadProxies'); }, sendGetHostResolverInfo: function() { // The browser will call receivedHostResolverInfo on completion. this.send('getHostResolverInfo'); }, sendRunIPv6Probe: function() { this.send('onRunIPv6Probe'); }, sendClearBadProxies: function() { this.send('clearBadProxies'); }, sendClearHostResolverCache: function() { this.send('clearHostResolverCache'); }, sendClearBrowserCache: function() { this.send('clearBrowserCache'); }, sendClearAllCache: function() { this.sendClearHostResolverCache(); this.sendClearBrowserCache(); }, sendStartConnectionTests: function(url) { this.send('startConnectionTests', [url]); }, sendHSTSQuery: function(domain) { this.send('hstsQuery', [domain]); }, sendHSTSAdd: function(domain, include_subdomains, pins) { this.send('hstsAdd', [domain, include_subdomains, pins]); }, sendHSTSDelete: function(domain) { this.send('hstsDelete', [domain]); }, sendGetHttpCacheInfo: function() { this.send('getHttpCacheInfo'); }, sendGetSocketPoolInfo: function() { this.send('getSocketPoolInfo'); }, sendGetSessionNetworkStats: function() { this.send('getSessionNetworkStats'); }, sendGetHistoricNetworkStats: function() { this.send('getHistoricNetworkStats'); }, sendCloseIdleSockets: function() { this.send('closeIdleSockets'); }, sendFlushSocketPools: function() { this.send('flushSocketPools'); }, sendGetSpdySessionInfo: function() { this.send('getSpdySessionInfo'); }, sendGetSpdyStatus: function() { this.send('getSpdyStatus'); }, sendGetSpdyAlternateProtocolMappings: function() { this.send('getSpdyAlternateProtocolMappings'); }, sendGetServiceProviders: function() { this.send('getServiceProviders'); }, sendGetPrerenderInfo: function() { this.send('getPrerenderInfo'); }, enableIPv6: function() { this.send('enableIPv6'); }, setLogLevel: function(logLevel) { this.send('setLogLevel', ['' + logLevel]); }, refreshSystemLogs: function() { this.send('refreshSystemLogs'); }, getSystemLog: function(log_key, cellId) { this.send('getSystemLog', [log_key, cellId]); }, importONCFile: function(fileContent, passcode) { this.send('importONCFile', [fileContent, passcode]); }, storeDebugLogs: function() { this.send('storeDebugLogs'); }, setNetworkDebugMode: function(subsystem) { this.send('setNetworkDebugMode', [subsystem]); }, sendGetHttpPipeliningStatus: function() { this.send('getHttpPipeliningStatus'); }, //-------------------------------------------------------------------------- // Messages received from the browser. //-------------------------------------------------------------------------- receive: function(command, params) { // Does nothing if disabled. if (this.disabled_) return; // If no constants have been received, and params does not contain the // constants, delay handling the data. if (Constants == null && command != 'receivedConstants') { this.earlyReceivedData_.push({ command: command, params: params }); return; } this[command](params); // Handle any data that was received early in the order it was received, // once the constants have been processed. if (this.earlyReceivedData_ != null) { for (var i = 0; i < this.earlyReceivedData_.length; i++) { var command = this.earlyReceivedData_[i]; this[command.command](command.params); } this.earlyReceivedData_ = null; } }, receivedConstants: function(constants) { for (var i = 0; i < this.constantsObservers_.length; i++) this.constantsObservers_[i].onReceivedConstants(constants); }, receivedLogEntries: function(logEntries) { EventsTracker.getInstance().addLogEntries(logEntries); }, receivedProxySettings: function(proxySettings) { this.pollableDataHelpers_.proxySettings.update(proxySettings); }, receivedBadProxies: function(badProxies) { this.pollableDataHelpers_.badProxies.update(badProxies); }, receivedHostResolverInfo: function(hostResolverInfo) { this.pollableDataHelpers_.hostResolverInfo.update(hostResolverInfo); }, receivedSocketPoolInfo: function(socketPoolInfo) { this.pollableDataHelpers_.socketPoolInfo.update(socketPoolInfo); }, receivedSessionNetworkStats: function(sessionNetworkStats) { this.pollableDataHelpers_.sessionNetworkStats.update(sessionNetworkStats); }, receivedHistoricNetworkStats: function(historicNetworkStats) { this.pollableDataHelpers_.historicNetworkStats.update( historicNetworkStats); }, receivedSpdySessionInfo: function(spdySessionInfo) { this.pollableDataHelpers_.spdySessionInfo.update(spdySessionInfo); }, receivedSpdyStatus: function(spdyStatus) { this.pollableDataHelpers_.spdyStatus.update(spdyStatus); }, receivedSpdyAlternateProtocolMappings: function(spdyAlternateProtocolMappings) { this.pollableDataHelpers_.spdyAlternateProtocolMappings.update( spdyAlternateProtocolMappings); }, receivedServiceProviders: function(serviceProviders) { this.pollableDataHelpers_.serviceProviders.update(serviceProviders); }, receivedStartConnectionTestSuite: function() { for (var i = 0; i < this.connectionTestsObservers_.length; i++) this.connectionTestsObservers_[i].onStartedConnectionTestSuite(); }, receivedStartConnectionTestExperiment: function(experiment) { for (var i = 0; i < this.connectionTestsObservers_.length; i++) { this.connectionTestsObservers_[i].onStartedConnectionTestExperiment( experiment); } }, receivedCompletedConnectionTestExperiment: function(info) { for (var i = 0; i < this.connectionTestsObservers_.length; i++) { this.connectionTestsObservers_[i].onCompletedConnectionTestExperiment( info.experiment, info.result); } }, receivedCompletedConnectionTestSuite: function() { for (var i = 0; i < this.connectionTestsObservers_.length; i++) this.connectionTestsObservers_[i].onCompletedConnectionTestSuite(); }, receivedHSTSResult: function(info) { for (var i = 0; i < this.hstsObservers_.length; i++) this.hstsObservers_[i].onHSTSQueryResult(info); }, receivedONCFileParse: function(error) { for (var i = 0; i < this.crosONCFileParseObservers_.length; i++) this.crosONCFileParseObservers_[i].onONCFileParse(error); }, receivedStoreDebugLogs: function(status) { for (var i = 0; i < this.storeDebugLogsObservers_.length; i++) this.storeDebugLogsObservers_[i].onStoreDebugLogs(status); }, receivedSetNetworkDebugMode: function(status) { for (var i = 0; i < this.setNetworkDebugModeObservers_.length; i++) this.setNetworkDebugModeObservers_[i].onSetNetworkDebugMode(status); }, receivedHttpCacheInfo: function(info) { this.pollableDataHelpers_.httpCacheInfo.update(info); }, receivedPrerenderInfo: function(prerenderInfo) { this.pollableDataHelpers_.prerenderInfo.update(prerenderInfo); }, receivedHttpPipeliningStatus: function(httpPipeliningStatus) { this.pollableDataHelpers_.httpPipeliningStatus.update( httpPipeliningStatus); }, //-------------------------------------------------------------------------- /** * Prevents receiving/sending events to/from the browser. */ disable: function() { this.disabled_ = true; this.setPollInterval(0); }, /** * Returns true if the BrowserBridge has been disabled. */ isDisabled: function() { return this.disabled_; }, /** * Adds a listener of the proxy settings. |observer| will be called back * when data is received, through: * * observer.onProxySettingsChanged(proxySettings) * * |proxySettings| is a dictionary with (up to) two properties: * * "original" -- The settings that chrome was configured to use * (i.e. system settings.) * "effective" -- The "effective" proxy settings that chrome is using. * (decides between the manual/automatic modes of the * fetched settings). * * Each of these two configurations is formatted as a string, and may be * omitted if not yet initialized. * * If |ignoreWhenUnchanged| is true, data is only sent when it changes. * If it's false, data is sent whenever it's received from the browser. */ addProxySettingsObserver: function(observer, ignoreWhenUnchanged) { this.pollableDataHelpers_.proxySettings.addObserver(observer, ignoreWhenUnchanged); }, /** * Adds a listener of the proxy settings. |observer| will be called back * when data is received, through: * * observer.onBadProxiesChanged(badProxies) * * |badProxies| is an array, where each entry has the property: * badProxies[i].proxy_uri: String identify the proxy. * badProxies[i].bad_until: The time when the proxy stops being considered * bad. Note the time is in time ticks. */ addBadProxiesObserver: function(observer, ignoreWhenUnchanged) { this.pollableDataHelpers_.badProxies.addObserver(observer, ignoreWhenUnchanged); }, /** * Adds a listener of the host resolver info. |observer| will be called back * when data is received, through: * * observer.onHostResolverInfoChanged(hostResolverInfo) */ addHostResolverInfoObserver: function(observer, ignoreWhenUnchanged) { this.pollableDataHelpers_.hostResolverInfo.addObserver( observer, ignoreWhenUnchanged); }, /** * Adds a listener of the socket pool. |observer| will be called back * when data is received, through: * * observer.onSocketPoolInfoChanged(socketPoolInfo) */ addSocketPoolInfoObserver: function(observer, ignoreWhenUnchanged) { this.pollableDataHelpers_.socketPoolInfo.addObserver(observer, ignoreWhenUnchanged); }, /** * Adds a listener of the network session. |observer| will be called back * when data is received, through: * * observer.onSessionNetworkStatsChanged(sessionNetworkStats) */ addSessionNetworkStatsObserver: function(observer, ignoreWhenUnchanged) { this.pollableDataHelpers_.sessionNetworkStats.addObserver( observer, ignoreWhenUnchanged); }, /** * Adds a listener of persistent network session data. |observer| will be * called back when data is received, through: * * observer.onHistoricNetworkStatsChanged(historicNetworkStats) */ addHistoricNetworkStatsObserver: function(observer, ignoreWhenUnchanged) { this.pollableDataHelpers_.historicNetworkStats.addObserver( observer, ignoreWhenUnchanged); }, /** * Adds a listener of the SPDY info. |observer| will be called back * when data is received, through: * * observer.onSpdySessionInfoChanged(spdySessionInfo) */ addSpdySessionInfoObserver: function(observer, ignoreWhenUnchanged) { this.pollableDataHelpers_.spdySessionInfo.addObserver( observer, ignoreWhenUnchanged); }, /** * Adds a listener of the SPDY status. |observer| will be called back * when data is received, through: * * observer.onSpdyStatusChanged(spdyStatus) */ addSpdyStatusObserver: function(observer, ignoreWhenUnchanged) { this.pollableDataHelpers_.spdyStatus.addObserver(observer, ignoreWhenUnchanged); }, /** * Adds a listener of the AlternateProtocolMappings. |observer| will be * called back when data is received, through: * * observer.onSpdyAlternateProtocolMappingsChanged( * spdyAlternateProtocolMappings) */ addSpdyAlternateProtocolMappingsObserver: function(observer, ignoreWhenUnchanged) { this.pollableDataHelpers_.spdyAlternateProtocolMappings.addObserver( observer, ignoreWhenUnchanged); }, /** * Adds a listener of the service providers info. |observer| will be called * back when data is received, through: * * observer.onServiceProvidersChanged(serviceProviders) * * Will do nothing if on a platform other than Windows, as service providers * are only present on Windows. */ addServiceProvidersObserver: function(observer, ignoreWhenUnchanged) { if (this.pollableDataHelpers_.serviceProviders) { this.pollableDataHelpers_.serviceProviders.addObserver( observer, ignoreWhenUnchanged); } }, /** * Adds a listener for the progress of the connection tests. * The observer will be called back with: * * observer.onStartedConnectionTestSuite(); * observer.onStartedConnectionTestExperiment(experiment); * observer.onCompletedConnectionTestExperiment(experiment, result); * observer.onCompletedConnectionTestSuite(); */ addConnectionTestsObserver: function(observer) { this.connectionTestsObservers_.push(observer); }, /** * Adds a listener for the http cache info results. * The observer will be called back with: * * observer.onHttpCacheInfoChanged(info); */ addHttpCacheInfoObserver: function(observer, ignoreWhenUnchanged) { this.pollableDataHelpers_.httpCacheInfo.addObserver( observer, ignoreWhenUnchanged); }, /** * Adds a listener for the results of HSTS (HTTPS Strict Transport Security) * queries. The observer will be called back with: * * observer.onHSTSQueryResult(result); */ addHSTSObserver: function(observer) { this.hstsObservers_.push(observer); }, /** * Adds a listener for ONC file parse status. The observer will be called * back with: * * observer.onONCFileParse(error); */ addCrosONCFileParseObserver: function(observer) { this.crosONCFileParseObservers_.push(observer); }, /** * Adds a listener for storing log file status. The observer will be called * back with: * * observer.onStoreDebugLogs(status); */ addStoreDebugLogsObserver: function(observer) { this.storeDebugLogsObservers_.push(observer); }, /** * Adds a listener for network debugging mode status. The observer * will be called back with: * * observer.onSetNetworkDebugMode(status); */ addSetNetworkDebugModeObserver: function(observer) { this.setNetworkDebugModeObservers_.push(observer); }, /** * Adds a listener for the received constants event. |observer| will be * called back when the constants are received, through: * * observer.onReceivedConstants(constants); */ addConstantsObserver: function(observer) { this.constantsObservers_.push(observer); }, /** * Adds a listener for updated prerender info events * |observer| will be called back with: * * observer.onPrerenderInfoChanged(prerenderInfo); */ addPrerenderInfoObserver: function(observer, ignoreWhenUnchanged) { this.pollableDataHelpers_.prerenderInfo.addObserver( observer, ignoreWhenUnchanged); }, /** * Adds a listener of HTTP pipelining status. |observer| will be called * back when data is received, through: * * observer.onHttpPipelineStatusChanged(httpPipeliningStatus) */ addHttpPipeliningStatusObserver: function(observer, ignoreWhenUnchanged) { this.pollableDataHelpers_.httpPipeliningStatus.addObserver( observer, ignoreWhenUnchanged); }, /** * If |force| is true, calls all startUpdate functions. Otherwise, just * runs updates with active observers. */ checkForUpdatedInfo: function(force) { for (var name in this.pollableDataHelpers_) { var helper = this.pollableDataHelpers_[name]; if (force || helper.hasActiveObserver()) helper.startUpdate(); } }, /** * Calls all startUpdate functions and, if |callback| is non-null, * calls it with the results of all updates. */ updateAllInfo: function(callback) { if (callback) new UpdateAllObserver(callback, this.pollableDataHelpers_); this.checkForUpdatedInfo(true); } }; /** * This is a helper class used by BrowserBridge, to keep track of: * - the list of observers interested in some piece of data. * - the last known value of that piece of data. * - the name of the callback method to invoke on observers. * - the update function. * @constructor */ function PollableDataHelper(observerMethodName, startUpdateFunction) { this.observerMethodName_ = observerMethodName; this.startUpdate = startUpdateFunction; this.observerInfos_ = []; } PollableDataHelper.prototype = { getObserverMethodName: function() { return this.observerMethodName_; }, isObserver: function(object) { for (var i = 0; i < this.observerInfos_.length; i++) { if (this.observerInfos_[i].observer === object) return true; } return false; }, /** * If |ignoreWhenUnchanged| is true, we won't send data again until it * changes. */ addObserver: function(observer, ignoreWhenUnchanged) { this.observerInfos_.push(new ObserverInfo(observer, ignoreWhenUnchanged)); }, removeObserver: function(observer) { for (var i = 0; i < this.observerInfos_.length; i++) { if (this.observerInfos_[i].observer === observer) { this.observerInfos_.splice(i, 1); return; } } }, /** * Helper function to handle calling all the observers, but ONLY if the data * has actually changed since last time or the observer has yet to receive * any data. This is used for data we received from browser on an update * loop. */ update: function(data) { var prevData = this.currentData_; var changed = false; // If the data hasn't changed since last time, will only need to notify // observers that have not yet received any data. if (!prevData || JSON.stringify(prevData) != JSON.stringify(data)) { changed = true; this.currentData_ = data; } // Notify the observers of the change, as needed. for (var i = 0; i < this.observerInfos_.length; i++) { var observerInfo = this.observerInfos_[i]; if (changed || !observerInfo.hasReceivedData || !observerInfo.ignoreWhenUnchanged) { observerInfo.observer[this.observerMethodName_](this.currentData_); observerInfo.hasReceivedData = true; } } }, /** * Returns true if one of the observers actively wants the data * (i.e. is visible). */ hasActiveObserver: function() { for (var i = 0; i < this.observerInfos_.length; i++) { if (this.observerInfos_[i].observer.isActive()) return true; } return false; } }; /** * This is a helper class used by PollableDataHelper, to keep track of * each observer and whether or not it has received any data. The * latter is used to make sure that new observers get sent data on the * update following their creation. * @constructor */ function ObserverInfo(observer, ignoreWhenUnchanged) { this.observer = observer; this.hasReceivedData = false; this.ignoreWhenUnchanged = ignoreWhenUnchanged; } /** * This is a helper class used by BrowserBridge to send data to * a callback once data from all polls has been received. * * It works by keeping track of how many polling functions have * yet to receive data, and recording the data as it it received. * * @constructor */ function UpdateAllObserver(callback, pollableDataHelpers) { this.callback_ = callback; this.observingCount_ = 0; this.updatedData_ = {}; for (var name in pollableDataHelpers) { ++this.observingCount_; var helper = pollableDataHelpers[name]; helper.addObserver(this); this[helper.getObserverMethodName()] = this.onDataReceived_.bind(this, helper, name); } } UpdateAllObserver.prototype = { isActive: function() { return true; }, onDataReceived_: function(helper, name, data) { helper.removeObserver(this); --this.observingCount_; this.updatedData_[name] = data; if (this.observingCount_ == 0) this.callback_(this.updatedData_); } }; return BrowserBridge; })(); // Copyright (c) 2012 The Chromium Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. var EventsTracker = (function() { 'use strict'; /** * This class keeps track of all NetLog events. * It receives events from the browser and when loading a log file, and passes * them on to all its observers. * * @constructor */ function EventsTracker() { assertFirstConstructorCall(EventsTracker); this.capturedEvents_ = []; this.observers_ = []; // Controls how large |capturedEvents_| can grow. this.softLimit_ = Infinity; this.hardLimit_ = Infinity; } cr.addSingletonGetter(EventsTracker); EventsTracker.prototype = { /** * Returns a list of all captured events. */ getAllCapturedEvents: function() { return this.capturedEvents_; }, /** * Returns the number of events that were captured. */ getNumCapturedEvents: function() { return this.capturedEvents_.length; }, /** * Deletes all the tracked events, and notifies any observers. */ deleteAllLogEntries: function() { this.capturedEvents_ = []; for (var i = 0; i < this.observers_.length; ++i) this.observers_[i].onAllLogEntriesDeleted(); }, /** * Adds captured events, and broadcasts them to any observers. */ addLogEntries: function(logEntries) { // When reloading a page, it's possible to receive events before // Constants. Discard those events, as they can cause the fake // "REQUEST_ALIVE" events for pre-existing requests not be the first // events for those requests. if (Constants == null) return; this.capturedEvents_ = this.capturedEvents_.concat(logEntries); for (var i = 0; i < this.observers_.length; ++i) { this.observers_[i].onReceivedLogEntries(logEntries); } // Check that we haven't grown too big. If so, toss out older events. if (this.getNumCapturedEvents() > this.hardLimit_) { var originalEvents = this.capturedEvents_; this.deleteAllLogEntries(); // Delete the oldest events until we reach the soft limit. originalEvents.splice(0, originalEvents.length - this.softLimit_); this.addLogEntries(originalEvents); } }, /** * Adds a listener of log entries. |observer| will be called back when new * log data arrives or all entries are deleted: * * observer.onReceivedLogEntries(entries) * observer.onAllLogEntriesDeleted() */ addLogEntryObserver: function(observer) { this.observers_.push(observer); }, /** * Set bounds on the maximum number of events that will be tracked. This * helps to bound the total amount of memory usage, since otherwise * long-running capture sessions can exhaust the renderer's memory and * crash. * * Once |hardLimit| number of events have been captured we do a garbage * collection and toss out old events, bringing our count down to * |softLimit|. * * To log observers this will look like all the events got deleted, and * then subsequently a bunch of new events were received. In other words, it * behaves the same as if the user had simply started logging a bit later * in time! */ setLimits: function(softLimit, hardLimit) { if (hardLimit != Infinity && softLimit >= hardLimit) throw 'hardLimit must be greater than softLimit'; this.softLimit_ = softLimit; this.hardLimit_ = hardLimit; } }; return EventsTracker; })(); // Copyright (c) 2012 The Chromium Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. var SourceTracker = (function() { 'use strict'; /** * This class keeps track of all NetLog events, grouped into per-source * streams. It receives events from EventsTracker, and passes * them on to all its observers. * * @constructor */ function SourceTracker() { assertFirstConstructorCall(SourceTracker); // Observers that only want to receive lists of updated SourceEntries. this.sourceEntryObservers_ = []; // True when cookies and authentication information should be removed from // displayed events. When true, such information should be hidden from // all pages. this.privacyStripping_ = true; this.clearEntries_(); EventsTracker.getInstance().addLogEntryObserver(this); } cr.addSingletonGetter(SourceTracker); SourceTracker.prototype = { /** * Clears all log entries and SourceEntries and related state. */ clearEntries_: function() { // Used for sorting entries with automatically assigned IDs. this.maxReceivedSourceId_ = 0; // Next unique id to be assigned to a log entry without a source. // Needed to identify associated GUI elements, etc. this.nextSourcelessEventId_ = -1; // Ordered list of log entries. Needed to maintain original order when // generating log dumps this.capturedEvents_ = []; this.sourceEntries_ = {}; }, /** * Returns a list of all SourceEntries. */ getAllSourceEntries: function() { return this.sourceEntries_; }, /** * Returns the description of the specified SourceEntry, or an empty string * if it doesn't exist. */ getDescription: function(id) { var entry = this.getSourceEntry(id); if (entry) return entry.getDescription(); return ''; }, /** * Returns the specified SourceEntry. */ getSourceEntry: function(id) { return this.sourceEntries_[id]; }, /** * Sends each entry to all observers and updates |capturedEvents_|. * Also assigns unique ids to log entries without a source. */ onReceivedLogEntries: function(logEntries) { // List source entries with new log entries. Sorted chronologically, by // first new log entry. var updatedSourceEntries = []; var updatedSourceEntryIdMap = {}; for (var e = 0; e < logEntries.length; ++e) { var logEntry = logEntries[e]; // Assign unique ID, if needed. // TODO(mmenke): Remove this, and all other code to handle 0 source // IDs when M19 hits stable. if (logEntry.source.id == 0) { logEntry.source.id = this.nextSourcelessEventId_; --this.nextSourcelessEventId_; } else if (this.maxReceivedSourceId_ < logEntry.source.id) { this.maxReceivedSourceId_ = logEntry.source.id; } // Create/update SourceEntry object. var sourceEntry = this.sourceEntries_[logEntry.source.id]; if (!sourceEntry) { sourceEntry = new SourceEntry(logEntry, this.maxReceivedSourceId_); this.sourceEntries_[logEntry.source.id] = sourceEntry; } else { sourceEntry.update(logEntry); } // Add to updated SourceEntry list, if not already in it. if (!updatedSourceEntryIdMap[logEntry.source.id]) { updatedSourceEntryIdMap[logEntry.source.id] = sourceEntry; updatedSourceEntries.push(sourceEntry); } } this.capturedEvents_ = this.capturedEvents_.concat(logEntries); for (var i = 0; i < this.sourceEntryObservers_.length; ++i) { this.sourceEntryObservers_[i].onSourceEntriesUpdated( updatedSourceEntries); } }, /** * Called when all log events have been deleted. */ onAllLogEntriesDeleted: function() { this.clearEntries_(); for (var i = 0; i < this.sourceEntryObservers_.length; ++i) this.sourceEntryObservers_[i].onAllSourceEntriesDeleted(); }, /** * Sets the value of |privacyStripping_| and informs log observers * of the change. */ setPrivacyStripping: function(privacyStripping) { this.privacyStripping_ = privacyStripping; for (var i = 0; i < this.sourceEntryObservers_.length; ++i) { if (this.sourceEntryObservers_[i].onPrivacyStrippingChanged) this.sourceEntryObservers_[i].onPrivacyStrippingChanged(); } }, /** * Returns whether or not cookies and authentication information should be * displayed for events that contain them. */ getPrivacyStripping: function() { return this.privacyStripping_; }, /** * Adds a listener of SourceEntries. |observer| will be called back when * SourceEntries are added or modified, source entries are deleted, or * privacy stripping changes: * * observer.onSourceEntriesUpdated(sourceEntries) * observer.onAllSourceEntriesDeleted() * observer.onPrivacyStrippingChanged() */ addSourceEntryObserver: function(observer) { this.sourceEntryObservers_.push(observer); } }; return SourceTracker; })(); // Copyright (c) 2011 The Chromium Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. /** * This view implements a vertically split display with a draggable divider. * * <<-- sizer -->> * * +----------------------++----------------+ * | || | * | || | * | || | * | || | * | leftView || rightView | * | || | * | || | * | || | * | || | * | || | * +----------------------++----------------+ * * @param {!View} leftView The widget to position on the left. * @param {!View} rightView The widget to position on the right. * @param {!DivView} sizerView The widget that will serve as draggable divider. */ var ResizableVerticalSplitView = (function() { 'use strict'; // Minimum width to size panels to, in pixels. var MIN_PANEL_WIDTH = 50; // We inherit from View. var superClass = View; /** * @constructor */ function ResizableVerticalSplitView(leftView, rightView, sizerView) { // Call superclass's constructor. superClass.call(this); this.leftView_ = leftView; this.rightView_ = rightView; this.sizerView_ = sizerView; this.mouseDragging_ = false; this.touchDragging_ = false; // Setup the "sizer" so it can be dragged left/right to reposition the // vertical split. The start event must occur within the sizer's node, // but subsequent events may occur anywhere. var node = sizerView.getNode(); node.addEventListener('mousedown', this.onMouseDragSizerStart_.bind(this)); window.addEventListener('mousemove', this.onMouseDragSizer_.bind(this)); window.addEventListener('mouseup', this.onMouseDragSizerEnd_.bind(this)); node.addEventListener('touchstart', this.onTouchDragSizerStart_.bind(this)); window.addEventListener('touchmove', this.onTouchDragSizer_.bind(this)); window.addEventListener('touchend', this.onTouchDragSizerEnd_.bind(this)); window.addEventListener('touchcancel', this.onTouchDragSizerEnd_.bind(this)); } ResizableVerticalSplitView.prototype = { // Inherit the superclass's methods. __proto__: superClass.prototype, /** * Sets the width of the left view. * @param {Integer} px The number of pixels */ setLeftSplit: function(px) { this.leftSplit_ = px; }, /** * Repositions all of the elements to fit the window. */ setGeometry: function(left, top, width, height) { superClass.prototype.setGeometry.call(this, left, top, width, height); // If this is the first setGeometry(), initialize the split point at 50%. if (!this.leftSplit_) this.leftSplit_ = parseInt((width / 2).toFixed(0)); // Calculate the horizontal split points. var leftboxWidth = this.leftSplit_; var sizerWidth = this.sizerView_.getWidth(); var rightboxWidth = width - (leftboxWidth + sizerWidth); // Don't let the right pane get too small. if (rightboxWidth < MIN_PANEL_WIDTH) { rightboxWidth = MIN_PANEL_WIDTH; leftboxWidth = width - (sizerWidth + rightboxWidth); } // Position the boxes using calculated split points. this.leftView_.setGeometry(left, top, leftboxWidth, height); this.sizerView_.setGeometry(this.leftView_.getRight(), top, sizerWidth, height); this.rightView_.setGeometry(this.sizerView_.getRight(), top, rightboxWidth, height); }, show: function(isVisible) { superClass.prototype.show.call(this, isVisible); this.leftView_.show(isVisible); this.sizerView_.show(isVisible); this.rightView_.show(isVisible); }, /** * Called once the sizer is clicked on. Starts moving the sizer in response * to future mouse movement. */ onMouseDragSizerStart_: function(event) { this.mouseDragging_ = true; event.preventDefault(); }, /** * Called when the mouse has moved. */ onMouseDragSizer_: function(event) { if (!this.mouseDragging_) return; // If dragging has started, move the sizer. this.onDragSizer_(event.pageX); event.preventDefault(); }, /** * Called once the mouse has been released. */ onMouseDragSizerEnd_: function(event) { if (!this.mouseDragging_) return; // Dragging is over. this.mouseDragging_ = false; event.preventDefault(); }, /** * Called when the user touches the sizer. Starts moving the sizer in * response to future touch events. */ onTouchDragSizerStart_: function(event) { this.touchDragging_ = true; event.preventDefault(); }, /** * Called when the mouse has moved after dragging started. */ onTouchDragSizer_: function(event) { if (!this.touchDragging_) return; // If dragging has started, move the sizer. this.onDragSizer_(event.touches[0].pageX); event.preventDefault(); }, /** * Called once the user stops touching the screen. */ onTouchDragSizerEnd_: function(event) { if (!this.touchDragging_) return; // Dragging is over. this.touchDragging_ = false; event.preventDefault(); }, /** * Common code used for both mouse and touch dragging. */ onDragSizer_: function(pageX) { // Convert from page coordinates, to view coordinates. this.leftSplit_ = (pageX - this.getLeft()); // Avoid shrinking the left box too much. this.leftSplit_ = Math.max(this.leftSplit_, MIN_PANEL_WIDTH); // Avoid shrinking the right box too much. this.leftSplit_ = Math.min( this.leftSplit_, this.getWidth() - MIN_PANEL_WIDTH); // Force a layout with the new |leftSplit_|. this.setGeometry( this.getLeft(), this.getTop(), this.getWidth(), this.getHeight()); }, }; return ResizableVerticalSplitView; })(); // Copyright (c) 2012 The Chromium Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. /** * Dictionary of constants (Initialized soon after loading by data from browser, * updated on load log). The *Types dictionaries map strings to numeric IDs, * while the *TypeNames are the other way around. */ var EventType = null; var EventTypeNames = null; var EventPhase = null; var EventSourceType = null; var EventSourceTypeNames = null; var LogLevelType = null; var ClientInfo = null; var NetError = null; var LoadFlag = null; var LoadState = null; var AddressFamily = null; /** * Dictionary of all constants, used for saving log files. */ var Constants = null; /** * Object to communicate between the renderer and the browser. * @type {!BrowserBridge} */ var g_browser = null; /** * This class is the root view object of the page. It owns all the other * views, and manages switching between them. It is also responsible for * initializing the views and the BrowserBridge. */ var MainView = (function() { 'use strict'; // We inherit from HorizontalSplitView var superClass = HorizontalSplitView; /** * Main entry point. Called once the page has loaded. * @constructor */ function MainView() { assertFirstConstructorCall(MainView); if (hasTouchScreen()) document.body.classList.add('touch'); // This must be initialized before the tabs, so they can register as // observers. g_browser = BrowserBridge.getInstance(); // This must be the first constants observer, so other constants observers // can safely use the globals, rather than depending on walking through // the constants themselves. g_browser.addConstantsObserver(new ConstantsObserver()); // This view is a left navigation bar. this.categoryTabSwitcher_ = new TabSwitcherView(); var tabs = this.categoryTabSwitcher_; // Call superclass's constructor, initializing the view which lets you tab // between the different sub-views. superClass.call(this, new DivView(MainView.CATEGORY_TAB_HANDLES_ID), tabs); // Populate the main tabs. Even tabs that don't contain information for the // running OS should be created, so they can load log dumps from other // OSes. tabs.addTab(CaptureView.TAB_HANDLE_ID, CaptureView.getInstance(), false, true); tabs.addTab(ExportView.TAB_HANDLE_ID, ExportView.getInstance(), false, true); tabs.addTab(ImportView.TAB_HANDLE_ID, ImportView.getInstance(), false, true); tabs.addTab(ProxyView.TAB_HANDLE_ID, ProxyView.getInstance(), false, true); tabs.addTab(EventsView.TAB_HANDLE_ID, EventsView.getInstance(), false, true); tabs.addTab(TimelineView.TAB_HANDLE_ID, TimelineView.getInstance(), false, true); tabs.addTab(DnsView.TAB_HANDLE_ID, DnsView.getInstance(), false, true); tabs.addTab(SocketsView.TAB_HANDLE_ID, SocketsView.getInstance(), false, true); tabs.addTab(SpdyView.TAB_HANDLE_ID, SpdyView.getInstance(), false, true); tabs.addTab(HttpPipelineView.TAB_HANDLE_ID, HttpPipelineView.getInstance(), false, true); tabs.addTab(HttpCacheView.TAB_HANDLE_ID, HttpCacheView.getInstance(), false, true); tabs.addTab(ServiceProvidersView.TAB_HANDLE_ID, ServiceProvidersView.getInstance(), false, cr.isWindows); tabs.addTab(TestView.TAB_HANDLE_ID, TestView.getInstance(), false, true); tabs.addTab(HSTSView.TAB_HANDLE_ID, HSTSView.getInstance(), false, true); tabs.addTab(LogsView.TAB_HANDLE_ID, LogsView.getInstance(), false, cr.isChromeOS); tabs.addTab(BandwidthView.TAB_HANDLE_ID, BandwidthView.getInstance(), false, true); tabs.addTab(PrerenderView.TAB_HANDLE_ID, PrerenderView.getInstance(), false, true); tabs.addTab(CrosView.TAB_HANDLE_ID, CrosView.getInstance(), false, cr.isChromeOS); // Build a map from the anchor name of each tab handle to its "tab ID". // We will consider navigations to the #hash as a switch tab request. var anchorMap = {}; var tabIds = tabs.getAllTabIds(); for (var i = 0; i < tabIds.length; ++i) { var aNode = $(tabIds[i]); anchorMap[aNode.hash] = tabIds[i]; } // Default the empty hash to the data tab. anchorMap['#'] = anchorMap[''] = ExportView.TAB_HANDLE_ID; window.onhashchange = onUrlHashChange.bind(null, tabs, anchorMap); // Cut out a small vertical strip at the top of the window, to display // a high level status (i.e. if we are capturing events, or displaying a // log file). Below it we will position the main tabs and their content // area. this.statusView_ = StatusView.getInstance(this); var verticalSplitView = new VerticalSplitView(this.statusView_, this); this.statusView_.setLayoutParent(verticalSplitView); var windowView = new WindowView(verticalSplitView); // Trigger initial layout. windowView.resetGeometry(); // Select the initial view based on the current URL. window.onhashchange(); // Tell the browser that we are ready to start receiving log events. g_browser.sendReady(); } // IDs for special HTML elements in index.html MainView.CATEGORY_TAB_HANDLES_ID = 'category-tab-handles'; cr.addSingletonGetter(MainView); // Tracks if we're viewing a loaded log file, so views can behave // appropriately. Global so safe to call during construction. var isViewingLoadedLog = false; MainView.isViewingLoadedLog = function() { return isViewingLoadedLog; }; MainView.prototype = { // Inherit the superclass's methods. __proto__: superClass.prototype, // This is exposed both so the log import/export code can enumerate all the // tabs, and for testing. categoryTabSwitcher: function() { return this.categoryTabSwitcher_; }, /** * Prevents receiving/sending events to/from the browser, so loaded data * will not be mixed with current Chrome state. Also hides any interactive * HTML elements that send messages to the browser. Cannot be undone * without reloading the page. Must be called before passing loaded data * to the individual views. * * @param {String} opt_fileName The name of the log file that has been * loaded, if we're loading a log file. */ onLoadLog: function(opt_fileName) { isViewingLoadedLog = true; this.stopCapturing(); if (opt_fileName != undefined) { // If there's a file name, a log file was loaded, so swap out the status // bar to indicate we're no longer capturing events. Also disable // hiding cookies, so if the log dump has them, they'll be displayed. this.statusView_.switchToSubView('loaded').setFileName(opt_fileName); $(ExportView.PRIVACY_STRIPPING_CHECKBOX_ID).checked = false; SourceTracker.getInstance().setPrivacyStripping(false); } else { // Otherwise, the "Stop Capturing" button was presumably pressed. // Don't disable hiding cookies, so created log dumps won't have them, // unless the user toggles the option. this.statusView_.switchToSubView('halted'); } }, switchToViewOnlyMode: function() { // Since this won't be dumped to a file, we don't want to remove // cookies and credentials. log_util.createLogDumpAsync('', log_util.loadLogFile, false); }, stopCapturing: function() { g_browser.disable(); document.styleSheets[0].insertRule( '.hide-when-not-capturing { display: none; }'); } }; /** * Takes the current hash in form of "#tab¶m1=value1¶m2=value2&...". * Puts the parameters in an object, and passes the resulting object to * |categoryTabSwitcher|. Uses tab and |anchorMap| to find a tab ID, * which it also passes to the tab switcher. * * Parameters and values are decoded with decodeURIComponent(). */ function onUrlHashChange(categoryTabSwitcher, anchorMap) { var parameters = window.location.hash.split('&'); var tabId = anchorMap[parameters[0]]; if (!tabId) return; // Split each string except the first around the '='. var paramDict = null; for (var i = 1; i < parameters.length; i++) { var paramStrings = parameters[i].split('='); if (paramStrings.length != 2) continue; if (paramDict == null) paramDict = {}; var key = decodeURIComponent(paramStrings[0]); var value = decodeURIComponent(paramStrings[1]); paramDict[key] = value; } categoryTabSwitcher.switchToTab(tabId, paramDict); } return MainView; })(); function ConstantsObserver() {} /** * Loads all constants from |constants|. On failure, global dictionaries are * not modifed. * @param {Object} receivedConstants The map of received constants. */ ConstantsObserver.prototype.onReceivedConstants = function(receivedConstants) { if (!areValidConstants(receivedConstants)) return; Constants = receivedConstants; EventType = Constants.logEventTypes; EventTypeNames = makeInverseMap(EventType); EventPhase = Constants.logEventPhase; EventSourceType = Constants.logSourceType; EventSourceTypeNames = makeInverseMap(EventSourceType); LogLevelType = Constants.logLevelType; ClientInfo = Constants.clientInfo; LoadFlag = Constants.loadFlag; NetError = Constants.netError; AddressFamily = Constants.addressFamily; LoadState = Constants.loadState; timeutil.setTimeTickOffset(Constants.timeTickOffset); }; /** * Returns true if it's given a valid-looking constants object. * @param {Object} receivedConstants The received map of constants. * @return {boolean} True if the |receivedConstants| object appears valid. */ function areValidConstants(receivedConstants) { return typeof(receivedConstants) == 'object' && typeof(receivedConstants.logEventTypes) == 'object' && typeof(receivedConstants.clientInfo) == 'object' && typeof(receivedConstants.logEventPhase) == 'object' && typeof(receivedConstants.logSourceType) == 'object' && typeof(receivedConstants.logLevelType) == 'object' && typeof(receivedConstants.loadFlag) == 'object' && typeof(receivedConstants.netError) == 'object' && typeof(receivedConstants.addressFamily) == 'object' && typeof(receivedConstants.timeTickOffset) == 'string' && typeof(receivedConstants.logFormatVersion) == 'number'; } /** * Returns the name for netError. * * Example: netErrorToString(-105) would return * "ERR_NAME_NOT_RESOLVED". * @param {number} netError The net error code. * @return {string} The name of the given error. */ function netErrorToString(netError) { var str = getKeyWithValue(NetError, netError); if (str == '?') return str; return 'ERR_' + str; } /** * Returns a string representation of |family|. * @param {number} family An AddressFamily * @return {string} A representation of the given family. */ function addressFamilyToString(family) { var str = getKeyWithValue(AddressFamily, family); // All the address family start with ADDRESS_FAMILY_*. // Strip that prefix since it is redundant and only clutters the output. return str.replace(/^ADDRESS_FAMILY_/, ''); } // Copyright (c) 2012 The Chromium Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. var timeutil = (function() { 'use strict'; /** * Offset needed to convert event times to Date objects. * Updated whenever constants are loaded. */ var timeTickOffset = 0; /** * Sets the offset used to convert tick counts to dates. */ function setTimeTickOffset(offset) { // Note that the subtraction by 0 is to cast to a number (probably a float // since the numbers are big). timeTickOffset = offset - 0; } /** * The browser gives us times in terms of "time ticks" in milliseconds. * This function converts the tick count to a Javascript "time", which is * the UTC time in milliseconds. * * @param {String} timeTicks A time represented in "time ticks". * @return {number} The Javascript time that |timeTicks| represents. */ function convertTimeTicksToTime(timeTicks) { return timeTickOffset + (timeTicks - 0); } /** * The browser gives us times in terms of "time ticks" in milliseconds. * This function converts the tick count to a Date() object. * * @param {String} timeTicks A time represented in "time ticks". * @return {Date} The time that |timeTicks| represents. */ function convertTimeTicksToDate(timeTicks) { return new Date(convertTimeTicksToTime(timeTicks)); } /** * Returns the current time. * * @return {number} Milliseconds since the Unix epoch. */ function getCurrentTime() { return (new Date()).getTime(); } /** * Adds an HTML representation of |date| to |parentNode|. * * @param {DomNode} parentNode The node that will contain the new node. * @param {Date} date The date to be displayed. * @return {DomNode} The new node containing the date/time. */ function addNodeWithDate(parentNode, date) { var span = addNodeWithText(parentNode, 'span', dateToString(date)); span.title = 't=' + date.getTime(); return span; } /** * Returns a string representation of |date|. * * @param {Date} date The date to be represented. * @return {String} A string representation of |date|. */ function dateToString(date) { var dateStr = date.getFullYear() + '-' + zeroPad_(date.getMonth() + 1, 2) + '-' + zeroPad_(date.getDate(), 2); var timeStr = zeroPad_(date.getHours(), 2) + ':' + zeroPad_(date.getMinutes(), 2) + ':' + zeroPad_(date.getSeconds(), 2) + '.' + zeroPad_(date.getMilliseconds(), 3); return dateStr + ' ' + timeStr; } /** * Prefixes enough zeros to |num| so that it has length |len|. * @param {number} num The number to be padded. * @param {number} len The desired length of the returned string. * @return {string} The zero-padded representation of |num|. */ function zeroPad_(num, len) { var str = num + ''; while (str.length < len) str = '0' + str; return str; } return { setTimeTickOffset: setTimeTickOffset, convertTimeTicksToTime: convertTimeTicksToTime, convertTimeTicksToDate: convertTimeTicksToDate, getCurrentTime: getCurrentTime, addNodeWithDate: addNodeWithDate, dateToString: dateToString }; })(); // Copyright (c) 2012 The Chromium Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. log_util = (function() { 'use strict'; /** * Creates a new log dump. |events| is a list of all events, |polledData| is * an object containing the results of each poll, |tabData| is an object * containing data for individual tabs, |date| is the time the dump was * created, as a formatted string, and |privacyStripping| is whether or not * private information should be removed from the generated dump. * * Returns the new log dump as an object. Resulting object may have a null * |numericDate|. * * TODO(eroman): Use javadoc notation for these parameters. * * Log dumps are just JSON objects containing five values: * * |userComments| User-provided notes describing what this dump file is * about. * |constants| needed to interpret the data. This also includes some * browser state information. * |events| from the NetLog. * |polledData| from each PollableDataHelper available on the source OS. * |tabData| containing any tab-specific state that's not present in * |polledData|. * * |polledData| and |tabData| may be empty objects, or may be missing data for * tabs not present on the OS the log is from. */ function createLogDump(userComments, constants, events, polledData, tabData, numericDate, privacyStripping) { if (privacyStripping) events = events.map(stripCookiesAndLoginInfo); var logDump = { 'userComments': userComments, 'constants': constants, 'events': events, 'polledData': polledData, 'tabData': tabData }; // Not technically client info, but it's used at the same point in the code. if (numericDate && constants.clientInfo) { constants.clientInfo.numericDate = numericDate; } return logDump; } /** * Returns a new log dump created using the polled data and date from the * |oldLogDump|. The other parts of the log dump come from current * net-internals state. */ function createUpdatedLogDump(userComments, oldLogDump, privacyStripping) { var numericDate = null; if (oldLogDump.constants.clientInfo && oldLogDump.constants.clientInfo.numericDate) { numericDate = oldLogDump.constants.clientInfo.numericDate; } var logDump = createLogDump( userComments, Constants, EventsTracker.getInstance().getAllCapturedEvents(), oldLogDump.polledData, getTabData_(), numericDate, privacyStripping); return JSON.stringify(logDump, null, ' '); } /** * Creates a full log dump using |polledData| and the return value of each * tab's saveState function and passes it to |callback|. */ function onUpdateAllCompleted(userComments, callback, privacyStripping, polledData) { var logDump = createLogDump( userComments, Constants, EventsTracker.getInstance().getAllCapturedEvents(), polledData, getTabData_(), timeutil.getCurrentTime(), privacyStripping); callback(JSON.stringify(logDump, null, ' ')); } /** * Called to create a new log dump. Must not be called once a dump has been * loaded. Once a log dump has been created, |callback| is passed the dumped * text as a string. */ function createLogDumpAsync(userComments, callback, privacyStripping) { g_browser.updateAllInfo( onUpdateAllCompleted.bind(null, userComments, callback, privacyStripping)); } /** * Gather any tab-specific state information prior to creating a log dump. */ function getTabData_() { var tabData = {}; var categoryTabSwitcher = MainView.getInstance().categoryTabSwitcher(); var tabIds = categoryTabSwitcher.getAllTabIds(); for (var i = 0; i < tabIds.length; ++i) { var view = categoryTabSwitcher.findTabById(tabIds[i]).contentView; if (view.saveState) tabData[tabIds[i]] = view.saveState(); } } /** * Loads a full log dump. Returns a string containing a log of the load. * |opt_fileName| should always be given when loading from a file, instead of * from a log dump generated in-memory. * The process goes like this: * 1) Load constants. If this fails, or the version number can't be handled, * abort the load. If this step succeeds, the load cannot be aborted. * 2) Clear all events. Any event observers are informed of the clear as * normal. * 3) Call onLoadLogStart(polledData, tabData) for each view with an * onLoadLogStart function. This allows tabs to clear any extra state * that would affect the next step. |polledData| contains the data polled * for all helpers, but |tabData| contains only the data from that * specific tab. * 4) Add all events from the log file. * 5) Call onLoadLogFinish(polledData, tabData) for each view with an * onLoadLogFinish function. The arguments are the same as in step 3. If * there is no onLoadLogFinish function, it throws an exception, or it * returns false instead of true, the data dump is assumed to contain no * valid data for the tab, so the tab is hidden. Otherwise, the tab is * shown. */ function loadLogDump(logDump, opt_fileName) { // Perform minimal validity check, and abort if it fails. if (typeof(logDump) != 'object') return 'Load failed. Top level JSON data is not an object.'; // String listing text summary of load errors, if any. var errorString = ''; if (!areValidConstants(logDump.constants)) errorString += 'Invalid constants object.\n'; if (typeof(logDump.events) != 'object') errorString += 'NetLog events missing.\n'; if (typeof(logDump.constants.logFormatVersion) != 'number') errorString += 'Invalid version number.\n'; if (errorString.length > 0) return 'Load failed:\n\n' + errorString; if (typeof(logDump.polledData) != 'object') logDump.polledData = {}; if (typeof(logDump.tabData) != 'object') logDump.tabData = {}; if (logDump.constants.logFormatVersion != Constants.logFormatVersion) { return 'Unable to load different log version.' + ' Found ' + logDump.constants.logFormatVersion + ', Expected ' + Constants.logFormatVersion; } g_browser.receivedConstants(logDump.constants); // Check for validity of each log entry, and then add the ones that pass. // Since the events are kept around, and we can't just hide a single view // on a bad event, we have more error checking for them than other data. var validEvents = []; var numDeprecatedPassiveEvents = 0; for (var eventIndex = 0; eventIndex < logDump.events.length; ++eventIndex) { var event = logDump.events[eventIndex]; if (typeof event == 'object' && typeof event.source == 'object' && typeof event.time == 'string' && typeof EventTypeNames[event.type] == 'string' && typeof EventSourceTypeNames[event.source.type] == 'string' && getKeyWithValue(EventPhase, event.phase) != '?') { if (event.wasPassivelyCaptured) { // NOTE: Up until Chrome 18, log dumps included "passively captured" // events. These are no longer supported, so skip past them // to avoid confusing the rest of the code. numDeprecatedPassiveEvents++; continue; } validEvents.push(event); } } // Make sure the loaded log contained an export date. If not we will // synthesize one. This can legitimately happen for dump files created // via command line flag, or for older dump formats (before Chrome 17). if (typeof logDump.constants.clientInfo.numericDate != 'number') { errorString += 'The log file is missing clientInfo.numericDate.\n'; if (validEvents.length > 0) { errorString += 'Synthesizing export date as time of last event captured.\n'; var lastEvent = validEvents[validEvents.length - 1]; ClientInfo.numericDate = timeutil.convertTimeTicksToDate(lastEvent.time).getTime(); } else { errorString += 'Can\'t guess export date!\n'; ClientInfo.numericDate = 0; } } // Prevent communication with the browser. Once the constants have been // loaded, it's safer to continue trying to load the log, even in the case // of bad data. MainView.getInstance().onLoadLog(opt_fileName); // Delete all events. This will also update all logObservers. EventsTracker.getInstance().deleteAllLogEntries(); // Inform all the views that a log file is being loaded, and pass in // view-specific saved state, if any. var categoryTabSwitcher = MainView.getInstance().categoryTabSwitcher(); var tabIds = categoryTabSwitcher.getAllTabIds(); for (var i = 0; i < tabIds.length; ++i) { var view = categoryTabSwitcher.findTabById(tabIds[i]).contentView; view.onLoadLogStart(logDump.polledData, logDump.tabData[tabIds[i]]); } EventsTracker.getInstance().addLogEntries(validEvents); var numInvalidEvents = logDump.events.length - (validEvents.length + numDeprecatedPassiveEvents); if (numInvalidEvents > 0) { errorString += 'Unable to load ' + numInvalidEvents + ' events, due to invalid data.\n\n'; } if (numDeprecatedPassiveEvents > 0) { errorString += 'Discarded ' + numDeprecatedPassiveEvents + ' passively collected events. Use an older version of Chrome to' + ' load this dump if you want to see them.\n\n'; } // Update all views with data from the file. Show only those views which // successfully load the data. for (var i = 0; i < tabIds.length; ++i) { var view = categoryTabSwitcher.findTabById(tabIds[i]).contentView; var showView = false; // The try block eliminates the need for checking every single value // before trying to access it. try { if (view.onLoadLogFinish(logDump.polledData, logDump.tabData[tabIds[i]], logDump)) { showView = true; } } catch (error) { } categoryTabSwitcher.showTabHandleNode(tabIds[i], showView); } return errorString + 'Log loaded.'; } /** * Loads a log dump from the string |logFileContents|, which can be either a * full net-internals dump, or a NetLog dump only. Returns a string * containing a log of the load. */ function loadLogFile(logFileContents, fileName) { // Try and parse the log dump as a single JSON string. If this succeeds, // it's most likely a full log dump. Otherwise, it may be a dump created by // --log-net-log. var parsedDump = null; try { parsedDump = JSON.parse(logFileContents); } catch (error) { try { // We may have a --log-net-log=blah log dump. If so, remove the comma // after the final good entry, and add the necessary close brackets. var end = Math.max(logFileContents.lastIndexOf(',\n'), logFileContents.lastIndexOf(',\r')); if (end != -1) parsedDump = JSON.parse(logFileContents.substring(0, end) + ']}'); } catch (error2) { } } if (!parsedDump) return 'Unable to parse log dump as JSON file.'; return loadLogDump(parsedDump, fileName); } // Exports. return { createUpdatedLogDump: createUpdatedLogDump, createLogDumpAsync: createLogDumpAsync, loadLogFile: loadLogFile }; })(); // Copyright (c) 2012 The Chromium Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. /** * The status view at the top of the page when in capturing mode. */ var CaptureStatusView = (function() { 'use strict'; // We inherit from DivView. var superClass = DivView; function CaptureStatusView() { superClass.call(this, CaptureStatusView.MAIN_BOX_ID); $(CaptureStatusView.STOP_BUTTON_ID).onclick = switchToViewOnlyMode_; $(CaptureStatusView.RESET_BUTTON_ID).onclick = EventsTracker.getInstance().deleteAllLogEntries.bind( EventsTracker.getInstance()); $(CaptureStatusView.CLEAR_CACHE_BUTTON_ID).onclick = g_browser.sendClearAllCache.bind(g_browser); $(CaptureStatusView.FLUSH_SOCKETS_BUTTON_ID).onclick = g_browser.sendFlushSocketPools.bind(g_browser); $(CaptureStatusView.TOGGLE_EXTRAS_ID).onclick = this.toggleExtras_.bind(this); this.capturedEventsCountBox_ = $(CaptureStatusView.CAPTURED_EVENTS_COUNT_ID); this.updateEventCounts_(); EventsTracker.getInstance().addLogEntryObserver(this); } // IDs for special HTML elements in status_view.html CaptureStatusView.MAIN_BOX_ID = 'capture-status-view'; CaptureStatusView.STOP_BUTTON_ID = 'capture-status-view-stop'; CaptureStatusView.RESET_BUTTON_ID = 'capture-status-view-reset'; CaptureStatusView.CLEAR_CACHE_BUTTON_ID = 'capture-status-view-clear-cache'; CaptureStatusView.FLUSH_SOCKETS_BUTTON_ID = 'capture-status-view-flush-sockets'; CaptureStatusView.CAPTURED_EVENTS_COUNT_ID = 'capture-status-view-captured-events-count'; CaptureStatusView.TOGGLE_EXTRAS_ID = 'capture-status-view-toggle-extras'; CaptureStatusView.EXTRAS_ID = 'capture-status-view-extras'; CaptureStatusView.prototype = { // Inherit the superclass's methods. __proto__: superClass.prototype, /** * Called whenever new log entries have been received. */ onReceivedLogEntries: function(logEntries) { this.updateEventCounts_(); }, /** * Called whenever all log events are deleted. */ onAllLogEntriesDeleted: function() { this.updateEventCounts_(); }, /** * Updates the counters showing how many events have been captured. */ updateEventCounts_: function() { this.capturedEventsCountBox_.textContent = EventsTracker.getInstance().getNumCapturedEvents(); }, /** * Toggles the visibility of the "extras" action bar. */ toggleExtras_: function() { var toggle = $(CaptureStatusView.TOGGLE_EXTRAS_ID); var extras = $(CaptureStatusView.EXTRAS_ID); var isVisible = extras.style.display == ''; // Toggle between the left-facing triangle and right-facing triange. toggle.className = isVisible ? 'capture-status-view-rotateleft' : 'capture-status-view-rotateright'; setNodeDisplay(extras, !isVisible); } }; /** * Calls the corresponding function of MainView. This is needed because we * can't call MainView.getInstance() in the constructor, as it hasn't been * created yet. */ function switchToViewOnlyMode_() { MainView.getInstance().switchToViewOnlyMode(); } return CaptureStatusView; })(); // Copyright (c) 2012 The Chromium Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. /** * The status view at the top of the page when viewing a loaded dump file. */ var LoadedStatusView = (function() { 'use strict'; // We inherit from DivView. var superClass = DivView; function LoadedStatusView() { superClass.call(this, LoadedStatusView.MAIN_BOX_ID); } LoadedStatusView.MAIN_BOX_ID = 'loaded-status-view'; LoadedStatusView.DUMP_FILE_NAME_ID = 'loaded-status-view-dump-file-name'; LoadedStatusView.prototype = { // Inherit the superclass's methods. __proto__: superClass.prototype, setFileName: function(fileName) { $(LoadedStatusView.DUMP_FILE_NAME_ID).innerText = fileName; } }; return LoadedStatusView; })(); // Copyright (c) 2012 The Chromium Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. /** * The status view at the top of the page after stopping capturing. */ var HaltedStatusView = (function() { 'use strict'; // We inherit from DivView. var superClass = DivView; function HaltedStatusView() { superClass.call(this, HaltedStatusView.MAIN_BOX_ID); } HaltedStatusView.MAIN_BOX_ID = 'halted-status-view'; HaltedStatusView.prototype = { // Inherit the superclass's methods. __proto__: superClass.prototype }; return HaltedStatusView; })(); // Copyright (c) 2012 The Chromium Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. /** * The status view at the top of the page. It displays what mode net-internals * is in (capturing, viewing only, viewing loaded log), and may have extra * information and actions depending on the mode. */ var StatusView = (function() { 'use strict'; // We inherit from View. var superClass = View; /** * Main entry point. Called once the page has loaded. * @constructor */ function StatusView() { assertFirstConstructorCall(StatusView); superClass.call(this); this.subViews_ = { capture: new CaptureStatusView(), loaded: new LoadedStatusView(), halted: new HaltedStatusView() }; this.activeSubViewName_ = 'capture'; // Hide the non-active views. for (var k in this.subViews_) { if (k != this.activeSubViewName_) this.subViews_[k].show(false); } } cr.addSingletonGetter(StatusView); StatusView.prototype = { // Inherit the superclass's methods. __proto__: superClass.prototype, setGeometry: function(left, top, width, height) { superClass.prototype.setGeometry.call(this, left, top, width, height); this.getActiveSubView_().setGeometry(left, top, width, height); }, getHeight: function() { return this.getActiveSubView_().getHeight(); }, show: function(isVisible) { superClass.prototype.show.call(this, isVisible); this.getActiveSubView_().show(isVisible); }, setLayoutParent: function(view) { this.layoutParent_ = view; }, /** * Switch the active subview. */ switchToSubView: function(name) { if (!this.subViews_[name]) throw 'Invalid subview name: ' + name; var prevSubView = this.getActiveSubView_(); this.activeSubViewName_ = name; var newSubView = this.getActiveSubView_(); prevSubView.show(false); newSubView.show(this.isVisible()); // Since the subview's dimensions may have changed, re-trigger a layout // for our parent. var view = this.layoutParent_; view.setGeometry(view.getLeft(), view.getTop(), view.getWidth(), view.getHeight()); return newSubView; }, getActiveSubView_: function() { return this.subViews_[this.activeSubViewName_]; } }; return StatusView; })(); // Copyright (c) 2012 The Chromium Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. /** * This view displays information on the host resolver: * * - Shows the default address family. * - Has a button to enable IPv6, if it is disabled. * - Shows the current host cache contents. * - Has a button to clear the host cache. * - Shows the parameters used to construct the host cache (capacity, ttl). */ // TODO(mmenke): Add links for each address entry to the corresponding NetLog // source. This could either be done by adding NetLog source ids // to cache entries, or tracking sources based on their type and // description. Former is simpler, latter may be useful // elsewhere as well. var DnsView = (function() { 'use strict'; // We inherit from DivView. var superClass = DivView; /** * @constructor */ function DnsView() { assertFirstConstructorCall(DnsView); // Call superclass's constructor. superClass.call(this, DnsView.MAIN_BOX_ID); $(DnsView.ENABLE_IPV6_BUTTON_ID).onclick = g_browser.enableIPv6.bind(g_browser); $(DnsView.IPV6_PROBE_BUTTON_ID).onclick = this.runIPv6Probe_.bind(this); $(DnsView.CLEAR_CACHE_BUTTON_ID).onclick = g_browser.sendClearHostResolverCache.bind(g_browser); // Used to track IPv6 probes. EventsTracker.getInstance().addLogEntryObserver(this); // ID of most recently started IPv6 probe job. Once the job completes, // set back to -1. this.ipv6ProbeJobSourceId_ = -1; // Register to receive changes to the host resolver info. g_browser.addHostResolverInfoObserver(this, false); } // ID for special HTML element in category_tabs.html DnsView.TAB_HANDLE_ID = 'tab-handle-dns'; // IDs for special HTML elements in dns_view.html DnsView.MAIN_BOX_ID = 'dns-view-tab-content'; DnsView.DEFAULT_FAMILY_SPAN_ID = 'dns-view-default-family'; DnsView.IPV6_DISABLED_SPAN_ID = 'dns-view-ipv6-disabled'; DnsView.ENABLE_IPV6_BUTTON_ID = 'dns-view-enable-ipv6'; DnsView.IPV6_PROBE_RUNNING_SPAN_ID = 'dns-view-ipv6-probe-running'; DnsView.IPV6_PROBE_COMPLETE_SPAN_ID = 'dns-view-ipv6-probe-complete'; DnsView.IPV6_PROBE_BUTTON_ID = 'dns-view-run-ipv6-probe'; DnsView.INTERNAL_DNS_ENABLED_SPAN_ID = 'dns-view-internal-dns-enabled'; DnsView.INTERNAL_DNS_INVALID_CONFIG_SPAN_ID = 'dns-view-internal-dns-invalid-config'; DnsView.INTERNAL_DNS_CONFIG_TBODY_ID = 'dns-view-internal-dns-config-tbody'; DnsView.CLEAR_CACHE_BUTTON_ID = 'dns-view-clear-cache'; DnsView.CAPACITY_SPAN_ID = 'dns-view-cache-capacity'; DnsView.ACTIVE_SPAN_ID = 'dns-view-cache-active'; DnsView.EXPIRED_SPAN_ID = 'dns-view-cache-expired'; DnsView.CACHE_TBODY_ID = 'dns-view-cache-tbody'; cr.addSingletonGetter(DnsView); DnsView.prototype = { // Inherit the superclass's methods. __proto__: superClass.prototype, onLoadLogStart: function(polledData, tabData, logDump) { // Clear information on whether or not an IPv6 probe is running. Needs // to be done before loading the events. this.setIPv6ProbeJobLookupRunning_(false, -1); }, onLoadLogFinish: function(data) { return this.onHostResolverInfoChanged(data.hostResolverInfo); }, onHostResolverInfoChanged: function(hostResolverInfo) { // Clear the existing values. $(DnsView.DEFAULT_FAMILY_SPAN_ID).innerHTML = ''; $(DnsView.CAPACITY_SPAN_ID).innerHTML = ''; $(DnsView.CACHE_TBODY_ID).innerHTML = ''; $(DnsView.ACTIVE_SPAN_ID).innerHTML = '0'; $(DnsView.EXPIRED_SPAN_ID).innerHTML = '0'; // Update fields containing async DNS configuration information. displayAsyncDnsConfig_(hostResolverInfo); // No info. if (!hostResolverInfo || !hostResolverInfo.cache) return false; var family = hostResolverInfo.default_address_family; addTextNode($(DnsView.DEFAULT_FAMILY_SPAN_ID), addressFamilyToString(family)); var ipv6Disabled = (family == AddressFamily.ADDRESS_FAMILY_IPV4); setNodeDisplay($(DnsView.IPV6_DISABLED_SPAN_ID), ipv6Disabled); // Fill in the basic cache information. var hostResolverCache = hostResolverInfo.cache; $(DnsView.CAPACITY_SPAN_ID).innerText = hostResolverCache.capacity; var expiredEntries = 0; // Date the cache was logged. This will be either now, when actively // logging data, or the date the log dump was created. var logDate; if (MainView.isViewingLoadedLog()) { logDate = new Date(ClientInfo.numericDate); } else { logDate = new Date(); } // Fill in the cache contents table. for (var i = 0; i < hostResolverCache.entries.length; ++i) { var e = hostResolverCache.entries[i]; var tr = addNode($(DnsView.CACHE_TBODY_ID), 'tr'); var hostnameCell = addNode(tr, 'td'); addTextNode(hostnameCell, e.hostname); var familyCell = addNode(tr, 'td'); addTextNode(familyCell, addressFamilyToString(e.address_family)); var addressesCell = addNode(tr, 'td'); if (e.error != undefined) { var errorText = e.error + ' (' + netErrorToString(e.error) + ')'; var errorNode = addTextNode(addressesCell, 'error: ' + errorText); addressesCell.classList.add('warning-text'); } else { addListToNode_(addNode(addressesCell, 'div'), e.addresses); } var expiresDate = timeutil.convertTimeTicksToDate(e.expiration); var expiresCell = addNode(tr, 'td'); timeutil.addNodeWithDate(expiresCell, expiresDate); if (logDate > timeutil.convertTimeTicksToDate(e.expiration)) { ++expiredEntries; var expiredSpan = addNode(expiresCell, 'span'); expiredSpan.classList.add('warning-text'); addTextNode(expiredSpan, ' [Expired]'); } } $(DnsView.ACTIVE_SPAN_ID).innerText = hostResolverCache.entries.length - expiredEntries; $(DnsView.EXPIRED_SPAN_ID).innerText = expiredEntries; return true; }, /** * Must be called whenever an IPv6 probe job starts or stops running. * @param {bool} running True if a probe job is running. * @param {sourceId} sourceId Source ID of the running probe job, if there * is one. -1 if |running| is false or we don't yet have the ID. */ setIPv6ProbeJobLookupRunning_: function(running, sourceId) { setNodeDisplay($(DnsView.IPV6_PROBE_RUNNING_SPAN_ID), running); setNodeDisplay($(DnsView.IPV6_PROBE_COMPLETE_SPAN_ID), !running); this.ipv6ProbeJobSourceId_ = sourceId; }, /** * Triggers a new IPv6 probe and displays the probe running message. */ runIPv6Probe_: function() { // Since there's no source ID yet, have to just use -1. We'll get the // ID when we see the start event for the probe. this.setIPv6ProbeJobLookupRunning_(true, -1); g_browser.sendRunIPv6Probe(); }, onReceivedLogEntries: function(logEntries) { for (var i = 0; i < logEntries.length; ++i) { if (logEntries[i].source.type != EventSourceType.IPV6_PROBE_JOB || logEntries[i].type != EventType.IPV6_PROBE_RUNNING) { continue; } // For IPV6_PROBE_JOB events, update the display depending on whether or // not a probe job is running. Only track the most recently started // probe job, as it will cancel any older jobs. if (logEntries[i].phase == EventPhase.PHASE_BEGIN) { this.setIPv6ProbeJobLookupRunning_(true, logEntries[i].source.id); } else if (logEntries[i].source.id == this.ipv6ProbeJobSourceId_) { this.setIPv6ProbeJobLookupRunning_(false, -1); g_browser.sendGetHostResolverInfo(); } } }, /** * Since the only thing that matters is the source ID of the active probe * job, which clearing events doesn't change, do nothing. */ onAllLogEntriesDeleted: function() { }, }; /** * Displays information corresponding to the current async DNS configuration. * @param {Object} hostResolverInfo The host resolver information. */ function displayAsyncDnsConfig_(hostResolverInfo) { // Clear the table. $(DnsView.INTERNAL_DNS_CONFIG_TBODY_ID).innerHTML = ''; // Figure out if the internal DNS resolver is disabled or has no valid // configuration information, and update display accordingly. var enabled = hostResolverInfo && hostResolverInfo.dns_config !== undefined; var noConfig = enabled && hostResolverInfo.dns_config.nameservers === undefined; $(DnsView.INTERNAL_DNS_ENABLED_SPAN_ID).innerText = enabled; setNodeDisplay($(DnsView.INTERNAL_DNS_INVALID_CONFIG_SPAN_ID), noConfig); // If the internal DNS resolver is disabled or has no valid configuration, // we're done. if (!enabled || noConfig) return; var dnsConfig = hostResolverInfo.dns_config; // Display nameservers first. var nameserverRow = addNode($(DnsView.INTERNAL_DNS_CONFIG_TBODY_ID), 'tr'); addNodeWithText(nameserverRow, 'th', 'nameservers'); addListToNode_(addNode(nameserverRow, 'td'), dnsConfig.nameservers); // Add everything else in |dnsConfig| to the table. for (var key in dnsConfig) { if (key == 'nameservers') continue; var tr = addNode($(DnsView.INTERNAL_DNS_CONFIG_TBODY_ID), 'tr'); addNodeWithText(tr, 'th', key); var td = addNode(tr, 'td'); // For lists, display each list entry on a separate line. if (typeof dnsConfig[key] == 'object' && dnsConfig[key].constructor == Array) { addListToNode_(td, dnsConfig[key]); continue; } addTextNode(td, dnsConfig[key]); } } /** * Takes a last of strings and adds them all to a DOM node, displaying them * on separate lines. * @param {DomNode} node The parent node. * @param {Array.} list List of strings to add to the node. */ function addListToNode_(node, list) { for (var i = 0; i < list.length; ++i) addNodeWithText(node, 'div', list[i]); } return DnsView; })(); // Copyright (c) 2012 The Chromium Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. var SourceRow = (function() { 'use strict'; /** * A SourceRow represents the row corresponding to a single SourceEntry * displayed by the EventsView. * * @constructor */ function SourceRow(parentView, sourceEntry) { this.parentView_ = parentView; this.sourceEntry_ = sourceEntry; this.isSelected_ = false; this.isMatchedByFilter_ = false; // Used to set CSS class for display. Must only be modified by calling // corresponding set functions. this.isSelected_ = false; this.isMouseOver_ = false; // Mirror sourceEntry's values, so we only update the DOM when necessary. this.isError_ = sourceEntry.isError(); this.isInactive_ = sourceEntry.isInactive(); this.description_ = sourceEntry.getDescription(); this.createRow_(); this.onSourceUpdated(); } SourceRow.prototype = { createRow_: function() { // Create a row. var tr = addNode(this.parentView_.tableBody_, 'tr'); tr._id = this.getSourceId(); tr.style.display = 'none'; this.row_ = tr; var selectionCol = addNode(tr, 'td'); var checkbox = addNode(selectionCol, 'input'); selectionCol.style.borderLeft = '0'; checkbox.type = 'checkbox'; var idCell = addNode(tr, 'td'); idCell.style.textAlign = 'right'; var typeCell = addNode(tr, 'td'); var descriptionCell = addNode(tr, 'td'); this.descriptionCell_ = descriptionCell; // Connect listeners. checkbox.onchange = this.onCheckboxToggled_.bind(this); var onclick = this.onClicked_.bind(this); idCell.onclick = onclick; typeCell.onclick = onclick; descriptionCell.onclick = onclick; tr.onmouseover = this.onMouseover_.bind(this); tr.onmouseout = this.onMouseout_.bind(this); // Set the cell values to match this source's data. if (this.getSourceId() >= 0) { addTextNode(idCell, this.getSourceId()); } else { addTextNode(idCell, '-'); } var sourceTypeString = this.sourceEntry_.getSourceTypeString(); addTextNode(typeCell, sourceTypeString); this.updateDescription_(); // Add a CSS classname specific to this source type (so CSS can specify // different stylings for different types). var sourceTypeClass = sourceTypeString.toLowerCase().replace(/_/g, '-'); this.row_.classList.add('source-' + sourceTypeClass); this.updateClass_(); }, onSourceUpdated: function() { if (this.sourceEntry_.isInactive() != this.isInactive_ || this.sourceEntry_.isError() != this.isError_) { this.updateClass_(); } if (this.description_ != this.sourceEntry_.getDescription()) this.updateDescription_(); // Update filters. var matchesFilter = this.matchesFilter(this.parentView_.currentFilter_); this.setIsMatchedByFilter(matchesFilter); }, /** * Changes |row_|'s class based on currently set flags. Clears any previous * class set by this method. This method is needed so that some styles * override others. */ updateClass_: function() { this.isInactive_ = this.sourceEntry_.isInactive(); this.isError_ = this.sourceEntry_.isError(); // Each element of this list contains a property of |this| and the // corresponding class name to set if that property is true. Entries // earlier in the list take precedence. var propertyNames = [ ['isSelected_', 'selected'], ['isMouseOver_', 'mouseover'], ['isError_', 'error'], ['isInactive_', 'inactive'], ]; // Loop through |propertyNames| in order, checking if each property // is true. For the first such property found, if any, add the // corresponding class to the SourceEntry's row. Remove classes // that correspond to any other property. var noStyleSet = true; for (var i = 0; i < propertyNames.length; ++i) { var setStyle = noStyleSet && this[propertyNames[i][0]]; if (setStyle) { this.row_.classList.add(propertyNames[i][1]); noStyleSet = false; } else { this.row_.classList.remove(propertyNames[i][1]); } } }, getSourceEntry: function() { return this.sourceEntry_; }, setIsMatchedByFilter: function(isMatchedByFilter) { if (this.isMatchedByFilter() == isMatchedByFilter) return; // No change. this.isMatchedByFilter_ = isMatchedByFilter; this.setFilterStyles(isMatchedByFilter); if (isMatchedByFilter) { this.parentView_.incrementPostfilterCount(1); } else { this.parentView_.incrementPostfilterCount(-1); // If we are filtering an entry away, make sure it is no longer // part of the selection. this.setSelected(false); } }, isMatchedByFilter: function() { return this.isMatchedByFilter_; }, setFilterStyles: function(isMatchedByFilter) { // Hide rows which have been filtered away. if (isMatchedByFilter) { this.row_.style.display = ''; } else { this.row_.style.display = 'none'; } }, matchesFilter: function(filter) { if (filter.isActive && this.isInactive_) return false; if (filter.isInactive && !this.isInactive_) return false; if (filter.isError && !this.isError_) return false; if (filter.isNotError && this.isError_) return false; // Check source type, if needed. if (filter.type) { var i; var sourceType = this.sourceEntry_.getSourceTypeString().toLowerCase(); for (i = 0; i < filter.type.length; ++i) { if (sourceType.search(filter.type[i]) != -1) break; } if (i == filter.type.length) return false; } // Check source ID, if needed. if (filter.id) { if (filter.id.indexOf(this.getSourceId() + '') == -1) return false; } if (filter.text == '') return true; // The description is not always contained in one of the log entries. if (this.description_.toLowerCase().indexOf(filter.text) != -1) return true; // Allow specifying source types by name. var sourceType = this.sourceEntry_.getSourceTypeString(); if (sourceType.toLowerCase().indexOf(filter.text) != -1) return true; return searchLogEntriesForText( filter.text, this.sourceEntry_.getLogEntries(), SourceTracker.getInstance().getPrivacyStripping()); }, isSelected: function() { return this.isSelected_; }, setSelected: function(isSelected) { if (isSelected == this.isSelected()) return; this.isSelected_ = isSelected; this.setSelectedStyles(isSelected); this.parentView_.modifySelectionArray(this.getSourceId(), isSelected); this.parentView_.onSelectionChanged(); }, setSelectedStyles: function(isSelected) { this.isSelected_ = isSelected; this.getSelectionCheckbox().checked = isSelected; this.updateClass_(); }, setMouseoverStyle: function(isMouseOver) { this.isMouseOver_ = isMouseOver; this.updateClass_(); }, onClicked_: function() { this.parentView_.clearSelection(); this.setSelected(true); if (this.isSelected()) this.parentView_.scrollToSourceId(this.getSourceId()); }, onMouseover_: function() { this.setMouseoverStyle(true); }, onMouseout_: function() { this.setMouseoverStyle(false); }, updateDescription_: function() { this.description_ = this.sourceEntry_.getDescription(); this.descriptionCell_.innerHTML = ''; addTextNode(this.descriptionCell_, this.description_); }, onCheckboxToggled_: function() { this.setSelected(this.getSelectionCheckbox().checked); if (this.isSelected()) this.parentView_.scrollToSourceId(this.getSourceId()); }, getSelectionCheckbox: function() { return this.row_.childNodes[0].firstChild; }, getSourceId: function() { return this.sourceEntry_.getSourceId(); }, /** * Returns source ID of the entry whose row is currently above this one's. * Returns null if no such node exists. */ getPreviousNodeSourceId: function() { var prevNode = this.row_.previousSibling; if (prevNode == null) return null; return prevNode._id; }, /** * Returns source ID of the entry whose row is currently below this one's. * Returns null if no such node exists. */ getNextNodeSourceId: function() { var nextNode = this.row_.nextSibling; if (nextNode == null) return null; return nextNode._id; }, /** * Moves current object's row before |entry|'s row. */ moveBefore: function(entry) { this.row_.parentNode.insertBefore(this.row_, entry.row_); }, /** * Moves current object's row after |entry|'s row. */ moveAfter: function(entry) { this.row_.parentNode.insertBefore(this.row_, entry.row_.nextSibling); } }; return SourceRow; })(); // Copyright (c) 2012 The Chromium Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. /** * EventsView displays a filtered list of all events sharing a source, and * a details pane for the selected sources. * * +----------------------++----------------+ * | filter box || | * +----------------------+| | * | || | * | || | * | || | * | || | * | source list || details | * | || view | * | || | * | || | * | || | * | || | * | || | * | || | * +----------------------++----------------+ */ var EventsView = (function() { 'use strict'; // How soon after updating the filter list the counter should be updated. var REPAINT_FILTER_COUNTER_TIMEOUT_MS = 0; // We inherit from View. var superClass = View; /* * @constructor */ function EventsView() { assertFirstConstructorCall(EventsView); // Call superclass's constructor. superClass.call(this); // Initialize the sub-views. var leftPane = new VerticalSplitView(new DivView(EventsView.TOPBAR_ID), new DivView(EventsView.LIST_BOX_ID)); this.detailsView_ = new DetailsView(EventsView.DETAILS_LOG_BOX_ID); this.splitterView_ = new ResizableVerticalSplitView( leftPane, this.detailsView_, new DivView(EventsView.SIZER_ID)); SourceTracker.getInstance().addSourceEntryObserver(this); this.tableBody_ = $(EventsView.TBODY_ID); this.filterInput_ = $(EventsView.FILTER_INPUT_ID); this.filterCount_ = $(EventsView.FILTER_COUNT_ID); this.filterInput_.addEventListener('search', this.onFilterTextChanged_.bind(this), true); $(EventsView.SELECT_ALL_ID).addEventListener( 'click', this.selectAll_.bind(this), true); $(EventsView.SORT_BY_ID_ID).addEventListener( 'click', this.sortById_.bind(this), true); $(EventsView.SORT_BY_SOURCE_TYPE_ID).addEventListener( 'click', this.sortBySourceType_.bind(this), true); $(EventsView.SORT_BY_DESCRIPTION_ID).addEventListener( 'click', this.sortByDescription_.bind(this), true); new MouseOverHelp(EventsView.FILTER_HELP_ID, EventsView.FILTER_HELP_HOVER_ID); // Sets sort order and filter. this.setFilter_(''); this.initializeSourceList_(); } // ID for special HTML element in category_tabs.html EventsView.TAB_HANDLE_ID = 'tab-handle-events'; // IDs for special HTML elements in events_view.html EventsView.TBODY_ID = 'events-view-source-list-tbody'; EventsView.FILTER_INPUT_ID = 'events-view-filter-input'; EventsView.FILTER_COUNT_ID = 'events-view-filter-count'; EventsView.FILTER_HELP_ID = 'events-view-filter-help'; EventsView.FILTER_HELP_HOVER_ID = 'events-view-filter-help-hover'; EventsView.SELECT_ALL_ID = 'events-view-select-all'; EventsView.SORT_BY_ID_ID = 'events-view-sort-by-id'; EventsView.SORT_BY_SOURCE_TYPE_ID = 'events-view-sort-by-source'; EventsView.SORT_BY_DESCRIPTION_ID = 'events-view-sort-by-description'; EventsView.DETAILS_LOG_BOX_ID = 'events-view-details-log-box'; EventsView.TOPBAR_ID = 'events-view-filter-box'; EventsView.LIST_BOX_ID = 'events-view-source-list'; EventsView.SIZER_ID = 'events-view-splitter-box'; cr.addSingletonGetter(EventsView); EventsView.prototype = { // Inherit the superclass's methods. __proto__: superClass.prototype, /** * Initializes the list of source entries. If source entries are already, * being displayed, removes them all in the process. */ initializeSourceList_: function() { this.currentSelectedRows_ = []; this.sourceIdToRowMap_ = {}; this.tableBody_.innerHTML = ''; this.numPrefilter_ = 0; this.numPostfilter_ = 0; this.invalidateFilterCounter_(); this.invalidateDetailsView_(); }, setGeometry: function(left, top, width, height) { superClass.prototype.setGeometry.call(this, left, top, width, height); this.splitterView_.setGeometry(left, top, width, height); }, show: function(isVisible) { superClass.prototype.show.call(this, isVisible); this.splitterView_.show(isVisible); }, getFilterText_: function() { return this.filterInput_.value; }, setFilterText_: function(filterText) { this.filterInput_.value = filterText; this.onFilterTextChanged_(); }, onFilterTextChanged_: function() { this.setFilter_(this.getFilterText_()); }, /** * Updates text in the details view when privacy stripping is toggled. */ onPrivacyStrippingChanged: function() { this.invalidateDetailsView_(); }, comparisonFuncWithReversing_: function(a, b) { var result = this.comparisonFunction_(a, b); if (this.doSortBackwards_) result *= -1; return result; }, sort_: function() { var sourceEntries = []; for (var id in this.sourceIdToRowMap_) { sourceEntries.push(this.sourceIdToRowMap_[id].getSourceEntry()); } sourceEntries.sort(this.comparisonFuncWithReversing_.bind(this)); // Reposition source rows from back to front. for (var i = sourceEntries.length - 2; i >= 0; --i) { var sourceRow = this.sourceIdToRowMap_[sourceEntries[i].getSourceId()]; var nextSourceId = sourceEntries[i + 1].getSourceId(); if (sourceRow.getNextNodeSourceId() != nextSourceId) { var nextSourceRow = this.sourceIdToRowMap_[nextSourceId]; sourceRow.moveBefore(nextSourceRow); } } }, /** * Looks for the first occurence of |directive|:parameter in |sourceText|. * Parameter can be an empty string. * * On success, returns an object with two fields: * |remainingText| - |sourceText| with |directive|:parameter removed, and excess whitespace deleted. * |parameter| - the parameter itself. * * On failure, returns null. */ parseDirective_: function(sourceText, directive) { // Adding a leading space allows a single regexp to be used, regardless of // whether or not the directive is at the start of the string. sourceText = ' ' + sourceText; var regExp = new RegExp('\\s+' + directive + ':(\\S*)\\s*', 'i'); var matchInfo = regExp.exec(sourceText); if (matchInfo == null) return null; return {'remainingText': sourceText.replace(regExp, ' ').trim(), 'parameter': matchInfo[1]}; }, /** * Just like parseDirective_, except can optionally be a '-' before or * the parameter, to negate it. Before is more natural, after * allows more convenient toggling. * * Returned value has the additional field |isNegated|, and a leading * '-' will be removed from |parameter|, if present. */ parseNegatableDirective_: function(sourceText, directive) { var matchInfo = this.parseDirective_(sourceText, directive); if (matchInfo == null) return null; // Remove any leading or trailing '-' from the directive. var negationInfo = /^(-?)(\S*?)$/.exec(matchInfo.parameter); matchInfo.parameter = negationInfo[2]; matchInfo.isNegated = (negationInfo[1] == '-'); return matchInfo; }, /** * Parse any "sort:" directives, and update |comparisonFunction_| and * |doSortBackwards_|as needed. Note only the last valid sort directive * is used. * * Returns |filterText| with all sort directives removed, including * invalid ones. */ parseSortDirectives_: function(filterText) { this.comparisonFunction_ = compareSourceId; this.doSortBackwards_ = false; while (true) { var sortInfo = this.parseNegatableDirective_(filterText, 'sort'); if (sortInfo == null) break; var comparisonName = sortInfo.parameter.toLowerCase(); if (COMPARISON_FUNCTION_TABLE[comparisonName] != null) { this.comparisonFunction_ = COMPARISON_FUNCTION_TABLE[comparisonName]; this.doSortBackwards_ = sortInfo.isNegated; } filterText = sortInfo.remainingText; } return filterText; }, /** * Parse any "is:" directives, and update |filter| accordingly. * * Returns |filterText| with all "is:" directives removed, including * invalid ones. */ parseRestrictDirectives_: function(filterText, filter) { while (true) { var filterInfo = this.parseNegatableDirective_(filterText, 'is'); if (filterInfo == null) break; if (filterInfo.parameter == 'active') { if (!filterInfo.isNegated) { filter.isActive = true; } else { filter.isInactive = true; } } if (filterInfo.parameter == 'error') { if (!filterInfo.isNegated) { filter.isError = true; } else { filter.isNotError = true; } } filterText = filterInfo.remainingText; } return filterText; }, /** * Parses all directives that take arbitrary strings as input, * and updates |filter| accordingly. Directives of these types * are stored as lists. * * Returns |filterText| with all recognized directives removed. */ parseStringDirectives_: function(filterText, filter) { var directives = ['type', 'id']; for (var i = 0; i < directives.length; ++i) { while (true) { var directive = directives[i]; var filterInfo = this.parseDirective_(filterText, directive); if (filterInfo == null) break; // Split parameters around commas and remove empty elements. var parameters = filterInfo.parameter.split(','); parameters = parameters.filter(function(string) { return string.length > 0; }); // If there's already a matching filter, take the intersection. // This behavior primarily exists for tests. It is not correct // when one of the 'type' filters is a partial match. if (filter[directive]) { parameters = parameters.filter(function(string) { return filter[directive].indexOf(string) != -1; }); } filter[directive] = parameters; filterText = filterInfo.remainingText; } } return filterText; }, /* * Converts |filterText| into an object representing the filter. */ createFilter_: function(filterText) { var filter = {}; filterText = filterText.toLowerCase(); filterText = this.parseRestrictDirectives_(filterText, filter); filterText = this.parseStringDirectives_(filterText, filter); filter.text = filterText.trim(); return filter; }, setFilter_: function(filterText) { var lastComparisonFunction = this.comparisonFunction_; var lastDoSortBackwards = this.doSortBackwards_; filterText = this.parseSortDirectives_(filterText); if (lastComparisonFunction != this.comparisonFunction_ || lastDoSortBackwards != this.doSortBackwards_) { this.sort_(); } this.currentFilter_ = this.createFilter_(filterText); // Iterate through all of the rows and see if they match the filter. for (var id in this.sourceIdToRowMap_) { var entry = this.sourceIdToRowMap_[id]; entry.setIsMatchedByFilter(entry.matchesFilter(this.currentFilter_)); } }, /** * Repositions |sourceRow|'s in the table using an insertion sort. * Significantly faster than sorting the entire table again, when only * one entry has changed. */ insertionSort_: function(sourceRow) { // SourceRow that should be after |sourceRow|, if it needs // to be moved earlier in the list. var sourceRowAfter = sourceRow; while (true) { var prevSourceId = sourceRowAfter.getPreviousNodeSourceId(); if (prevSourceId == null) break; var prevSourceRow = this.sourceIdToRowMap_[prevSourceId]; if (this.comparisonFuncWithReversing_( sourceRow.getSourceEntry(), prevSourceRow.getSourceEntry()) >= 0) { break; } sourceRowAfter = prevSourceRow; } if (sourceRowAfter != sourceRow) { sourceRow.moveBefore(sourceRowAfter); return; } var sourceRowBefore = sourceRow; while (true) { var nextSourceId = sourceRowBefore.getNextNodeSourceId(); if (nextSourceId == null) break; var nextSourceRow = this.sourceIdToRowMap_[nextSourceId]; if (this.comparisonFuncWithReversing_( sourceRow.getSourceEntry(), nextSourceRow.getSourceEntry()) <= 0) { break; } sourceRowBefore = nextSourceRow; } if (sourceRowBefore != sourceRow) sourceRow.moveAfter(sourceRowBefore); }, /** * Called whenever SourceEntries are updated with new log entries. Updates * the corresponding table rows, sort order, and the details view as needed. */ onSourceEntriesUpdated: function(sourceEntries) { var isUpdatedSourceSelected = false; var numNewSourceEntries = 0; for (var i = 0; i < sourceEntries.length; ++i) { var sourceEntry = sourceEntries[i]; // Lookup the row. var sourceRow = this.sourceIdToRowMap_[sourceEntry.getSourceId()]; if (!sourceRow) { sourceRow = new SourceRow(this, sourceEntry); this.sourceIdToRowMap_[sourceEntry.getSourceId()] = sourceRow; ++numNewSourceEntries; } else { sourceRow.onSourceUpdated(); } if (sourceRow.isSelected()) isUpdatedSourceSelected = true; // TODO(mmenke): Fix sorting when sorting by duration. // Duration continuously increases for all entries that // are still active. This can result in incorrect // sorting, until sort_ is called. this.insertionSort_(sourceRow); } if (isUpdatedSourceSelected) this.invalidateDetailsView_(); if (numNewSourceEntries) this.incrementPrefilterCount(numNewSourceEntries); }, /** * Returns the SourceRow with the specified ID, if there is one. * Otherwise, returns undefined. */ getSourceRow: function(id) { return this.sourceIdToRowMap_[id]; }, /** * Called whenever all log events are deleted. */ onAllSourceEntriesDeleted: function() { this.initializeSourceList_(); }, /** * Called when either a log file is loaded, after clearing the old entries, * but before getting any new ones. */ onLoadLogStart: function() { // Needed to sort new sourceless entries correctly. this.maxReceivedSourceId_ = 0; }, onLoadLogFinish: function(data) { return true; }, incrementPrefilterCount: function(offset) { this.numPrefilter_ += offset; this.invalidateFilterCounter_(); }, incrementPostfilterCount: function(offset) { this.numPostfilter_ += offset; this.invalidateFilterCounter_(); }, onSelectionChanged: function() { this.invalidateDetailsView_(); }, clearSelection: function() { var prevSelection = this.currentSelectedRows_; this.currentSelectedRows_ = []; // Unselect everything that is currently selected. for (var i = 0; i < prevSelection.length; ++i) { prevSelection[i].setSelected(false); } this.onSelectionChanged(); }, selectAll_: function(event) { for (var id in this.sourceIdToRowMap_) { var sourceRow = this.sourceIdToRowMap_[id]; if (sourceRow.isMatchedByFilter()) { sourceRow.setSelected(true); } } event.preventDefault(); }, unselectAll_: function() { var entries = this.currentSelectedRows_.slice(0); for (var i = 0; i < entries.length; ++i) { entries[i].setSelected(false); } }, /** * If |params| includes a query, replaces the current filter and unselects. * all items. If it includes a selection, tries to select the relevant * item. */ setParameters: function(params) { if (params.q) { this.unselectAll_(); this.setFilterText_(params.q); } if (params.s) { var sourceRow = this.sourceIdToRowMap_[params.s]; if (sourceRow) { sourceRow.setSelected(true); this.scrollToSourceId(params.s); } } }, /** * Scrolls to the source indicated by |sourceId|, if displayed. */ scrollToSourceId: function(sourceId) { this.detailsView_.scrollToSourceId(sourceId); }, /** * If already using the specified sort method, flips direction. Otherwise, * removes pre-existing sort parameter before adding the new one. */ toggleSortMethod_: function(sortMethod) { // Remove old sort directives, if any. var filterText = this.parseSortDirectives_(this.getFilterText_()); // If already using specified sortMethod, sort backwards. if (!this.doSortBackwards_ && COMPARISON_FUNCTION_TABLE[sortMethod] == this.comparisonFunction_) sortMethod = '-' + sortMethod; filterText = 'sort:' + sortMethod + ' ' + filterText; this.setFilterText_(filterText.trim()); }, sortById_: function(event) { this.toggleSortMethod_('id'); }, sortBySourceType_: function(event) { this.toggleSortMethod_('source'); }, sortByDescription_: function(event) { this.toggleSortMethod_('desc'); }, /** * Modifies the map of selected rows to include/exclude the one with * |sourceId|, if present. Does not modify checkboxes or the LogView. * Should only be called by a SourceRow in response to its selection * state changing. */ modifySelectionArray: function(sourceId, addToSelection) { var sourceRow = this.sourceIdToRowMap_[sourceId]; if (!sourceRow) return; // Find the index for |sourceEntry| in the current selection list. var index = -1; for (var i = 0; i < this.currentSelectedRows_.length; ++i) { if (this.currentSelectedRows_[i] == sourceRow) { index = i; break; } } if (index != -1 && !addToSelection) { // Remove from the selection. this.currentSelectedRows_.splice(index, 1); } if (index == -1 && addToSelection) { this.currentSelectedRows_.push(sourceRow); } }, getSelectedSourceEntries_: function() { var sourceEntries = []; for (var i = 0; i < this.currentSelectedRows_.length; ++i) { sourceEntries.push(this.currentSelectedRows_[i].getSourceEntry()); } return sourceEntries; }, invalidateDetailsView_: function() { this.detailsView_.setData(this.getSelectedSourceEntries_()); }, invalidateFilterCounter_: function() { if (!this.outstandingRepaintFilterCounter_) { this.outstandingRepaintFilterCounter_ = true; window.setTimeout(this.repaintFilterCounter_.bind(this), REPAINT_FILTER_COUNTER_TIMEOUT_MS); } }, repaintFilterCounter_: function() { this.outstandingRepaintFilterCounter_ = false; this.filterCount_.innerHTML = ''; addTextNode(this.filterCount_, this.numPostfilter_ + ' of ' + this.numPrefilter_); } }; // end of prototype. // ------------------------------------------------------------------------ // Helper code for comparisons // ------------------------------------------------------------------------ var COMPARISON_FUNCTION_TABLE = { // sort: and sort:- are allowed '': compareSourceId, 'active': compareActive, 'desc': compareDescription, 'description': compareDescription, 'duration': compareDuration, 'id': compareSourceId, 'source': compareSourceType, 'type': compareSourceType }; /** * Sorts active entries first. If both entries are inactive, puts the one * that was active most recently first. If both are active, uses source ID, * which puts longer lived events at the top, and behaves better than using * duration or time of first event. */ function compareActive(source1, source2) { if (!source1.isInactive() && source2.isInactive()) return -1; if (source1.isInactive() && !source2.isInactive()) return 1; if (source1.isInactive()) { var deltaEndTime = source1.getEndTime() - source2.getEndTime(); if (deltaEndTime != 0) { // The one that ended most recently (Highest end time) should be sorted // first. return -deltaEndTime; } // If both ended at the same time, then odds are they were related events, // started one after another, so sort in the opposite order of their // source IDs to get a more intuitive ordering. return -compareSourceId(source1, source2); } return compareSourceId(source1, source2); } function compareDescription(source1, source2) { var source1Text = source1.getDescription().toLowerCase(); var source2Text = source2.getDescription().toLowerCase(); var compareResult = source1Text.localeCompare(source2Text); if (compareResult != 0) return compareResult; return compareSourceId(source1, source2); } function compareDuration(source1, source2) { var durationDifference = source2.getDuration() - source1.getDuration(); if (durationDifference) return durationDifference; return compareSourceId(source1, source2); } /** * For the purposes of sorting by source IDs, entries without a source * appear right after the SourceEntry with the highest source ID received * before the sourceless entry. Any ambiguities are resolved by ordering * the entries without a source by the order in which they were received. */ function compareSourceId(source1, source2) { var sourceId1 = source1.getSourceId(); if (sourceId1 < 0) sourceId1 = source1.getMaxPreviousEntrySourceId(); var sourceId2 = source2.getSourceId(); if (sourceId2 < 0) sourceId2 = source2.getMaxPreviousEntrySourceId(); if (sourceId1 != sourceId2) return sourceId1 - sourceId2; // One or both have a negative ID. In either case, the source with the // highest ID should be sorted first. return source2.getSourceId() - source1.getSourceId(); } function compareSourceType(source1, source2) { var source1Text = source1.getSourceTypeString(); var source2Text = source2.getSourceTypeString(); var compareResult = source1Text.localeCompare(source2Text); if (compareResult != 0) return compareResult; return compareSourceId(source1, source2); } return EventsView; })(); // Copyright (c) 2012 The Chromium Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. var DetailsView = (function() { 'use strict'; // We inherit from DivView. var superClass = DivView; /** * The DetailsView displays the "log" view. This class keeps track of the * selected SourceEntries, and repaints when they change. * * @constructor */ function DetailsView(boxId) { superClass.call(this, boxId); this.sourceEntries_ = []; // Map of source IDs to their corresponding DIVs. this.sourceIdToDivMap_ = {}; // True when there's an asychronous repaint outstanding. this.outstandingRepaint_ = false; // ID of source entry we should jump to after the oustanding repaint. // 0 if none, or there's no such repaint. this.outstandingScrollToId_ = 0; } // The delay between updates to repaint. var REPAINT_TIMEOUT_MS = 50; DetailsView.prototype = { // Inherit the superclass's methods. __proto__: superClass.prototype, setData: function(sourceEntries) { // Make a copy of the array (in case the caller mutates it), and sort it // by the source ID. this.sourceEntries_ = createSortedCopy_(sourceEntries); // Repaint the view. if (this.isVisible() && !this.outstandingRepaint_) { this.outstandingRepaint_ = true; window.setTimeout(this.repaint.bind(this), REPAINT_TIMEOUT_MS); } }, repaint: function() { this.outstandingRepaint_ = false; this.sourceIdToDivMap_ = {}; this.getNode().innerHTML = ''; var node = this.getNode(); for (var i = 0; i < this.sourceEntries_.length; ++i) { if (i != 0) addNode(node, 'hr'); var sourceEntry = this.sourceEntries_[i]; var div = addNode(node, 'div'); div.className = 'log-source-entry'; var p = addNode(div, 'p'); addNodeWithText(p, 'h4', sourceEntry.getSourceId() + ': ' + sourceEntry.getSourceTypeString()); if (sourceEntry.getDescription()) addNodeWithText(p, 'h4', sourceEntry.getDescription()); var logEntries = sourceEntry.getLogEntries(); var startDate = timeutil.convertTimeTicksToDate(logEntries[0].time); var startTimeDiv = addNodeWithText(p, 'div', 'Start Time: '); timeutil.addNodeWithDate(startTimeDiv, startDate); sourceEntry.printAsText(div); this.sourceIdToDivMap_[sourceEntry.getSourceId()] = div; } if (this.outstandingScrollToId_) { this.scrollToSourceId(this.outstandingScrollToId_); this.outstandingScrollToId_ = 0; } }, show: function(isVisible) { superClass.prototype.show.call(this, isVisible); if (isVisible) { this.repaint(); } else { this.getNode().innerHTML = ''; } }, /** * Scrolls to the source indicated by |sourceId|, if displayed. If a * repaint is outstanding, waits for it to complete before scrolling. */ scrollToSourceId: function(sourceId) { if (this.outstandingRepaint_) { this.outstandingScrollToId_ = sourceId; return; } var div = this.sourceIdToDivMap_[sourceId]; if (div) div.scrollIntoView(); } }; function createSortedCopy_(origArray) { var sortedArray = origArray.slice(0); sortedArray.sort(function(a, b) { return a.getSourceId() - b.getSourceId(); }); return sortedArray; } return DetailsView; })(); // Copyright (c) 2012 The Chromium Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. var SourceEntry = (function() { 'use strict'; /** * A SourceEntry gathers all log entries with the same source. * * @constructor */ function SourceEntry(logEntry, maxPreviousSourceId) { this.maxPreviousSourceId_ = maxPreviousSourceId; this.entries_ = []; this.description_ = ''; // Set to true on most net errors. this.isError_ = false; // If the first entry is a BEGIN_PHASE, set to false. // Set to true when an END_PHASE matching the first entry is encountered. this.isInactive_ = true; if (logEntry.phase == EventPhase.PHASE_BEGIN) this.isInactive_ = false; this.update(logEntry); } SourceEntry.prototype = { update: function(logEntry) { // Only the last event should have the same type first event, if (!this.isInactive_ && logEntry.phase == EventPhase.PHASE_END && logEntry.type == this.entries_[0].type) { this.isInactive_ = true; } // If we have a net error code, update |this.isError_| if apporpriate. if (logEntry.params) { var netErrorCode = logEntry.params.net_error; // Skip both cases where netErrorCode is undefined, and cases where it // is 0, indicating no actual error occurred. if (netErrorCode) { // Ignore error code caused by not finding an entry in the cache. if (logEntry.type != EventType.HTTP_CACHE_OPEN_ENTRY || netErrorCode != NetError.FAILED) { this.isError_ = true; } } } var prevStartEntry = this.getStartEntry_(); this.entries_.push(logEntry); var curStartEntry = this.getStartEntry_(); // If we just got the first entry for this source. if (prevStartEntry != curStartEntry) this.updateDescription_(); }, updateDescription_: function() { var e = this.getStartEntry_(); this.description_ = ''; if (!e) return; if (e.source.type == EventSourceType.NONE) { // NONE is what we use for global events that aren't actually grouped // by a "source ID", so we will just stringize the event's type. this.description_ = EventTypeNames[e.type]; return; } if (e.params == undefined) { return; } switch (e.source.type) { case EventSourceType.URL_REQUEST: case EventSourceType.SOCKET_STREAM: case EventSourceType.HTTP_STREAM_JOB: this.description_ = e.params.url; break; case EventSourceType.CONNECT_JOB: this.description_ = e.params.group_name; break; case EventSourceType.HOST_RESOLVER_IMPL_REQUEST: case EventSourceType.HOST_RESOLVER_IMPL_JOB: case EventSourceType.HOST_RESOLVER_IMPL_PROC_TASK: this.description_ = e.params.host; break; case EventSourceType.DISK_CACHE_ENTRY: case EventSourceType.MEMORY_CACHE_ENTRY: this.description_ = e.params.key; break; case EventSourceType.SPDY_SESSION: if (e.params.host) this.description_ = e.params.host + ' (' + e.params.proxy + ')'; break; case EventSourceType.HTTP_PIPELINED_CONNECTION: if (e.params.host_and_port) this.description_ = e.params.host_and_port; break; case EventSourceType.SOCKET: // Use description of parent source, if any. if (e.params.source_dependency != undefined) { var parentId = e.params.source_dependency.id; this.description_ = SourceTracker.getInstance().getDescription(parentId); } break; case EventSourceType.UDP_SOCKET: if (e.params.address != undefined) { this.description_ = e.params.address; // If the parent of |this| is a HOST_RESOLVER_IMPL_JOB, use // ' []'. if (this.entries_[0].type == EventType.SOCKET_ALIVE && this.entries_[0].params && this.entries_[0].params.source_dependency != undefined) { var parentId = this.entries_[0].params.source_dependency.id; var parent = SourceTracker.getInstance().getSourceEntry(parentId); if (parent && parent.getSourceType() == EventSourceType.HOST_RESOLVER_IMPL_JOB && parent.getDescription().length > 0) { this.description_ += ' [' + parent.getDescription() + ']'; } } } break; case EventSourceType.ASYNC_HOST_RESOLVER_REQUEST: case EventSourceType.DNS_TRANSACTION: this.description_ = e.params.hostname; break; case EventSourceType.DOWNLOAD: switch (e.type) { case EventType.DOWNLOAD_FILE_RENAMED: this.description_ = e.params.new_filename; break; case EventType.DOWNLOAD_FILE_OPENED: this.description_ = e.params.file_name; break; case EventType.DOWNLOAD_ITEM_ACTIVE: this.description_ = e.params.file_name; break; } break; case EventSourceType.FILESTREAM: this.description_ = e.params.file_name; break; case EventSourceType.IPV6_PROBE_JOB: if (e.type == EventType.IPV6_PROBE_RUNNING && e.phase == EventPhase.PHASE_END) { this.description_ = e.params.ipv6_supported ? 'IPv6 Supported' : 'IPv6 Not Supported'; } break; } if (this.description_ == undefined) this.description_ = ''; }, /** * Returns a description for this source log stream, which will be displayed * in the list view. Most often this is a URL that identifies the request, * or a hostname for a connect job, etc... */ getDescription: function() { return this.description_; }, /** * Returns the starting entry for this source. Conceptually this is the * first entry that was logged to this source. However, we skip over the * TYPE_REQUEST_ALIVE entries which wrap TYPE_URL_REQUEST_START_JOB / * TYPE_SOCKET_STREAM_CONNECT. */ getStartEntry_: function() { if (this.entries_.length < 1) return undefined; if (this.entries_[0].source.type == EventSourceType.FILESTREAM) { var e = this.findLogEntryByType_(EventType.FILE_STREAM_OPEN); if (e != undefined) return e; } if (this.entries_[0].source.type == EventSourceType.DOWNLOAD) { // If any rename occurred, use the last name e = this.findLastLogEntryStartByType_( EventType.DOWNLOAD_FILE_RENAMED); if (e != undefined) return e; // Otherwise, if the file was opened, use that name e = this.findLogEntryByType_(EventType.DOWNLOAD_FILE_OPENED); if (e != undefined) return e; // History items are never opened, so use the activation info e = this.findLogEntryByType_(EventType.DOWNLOAD_ITEM_ACTIVE); if (e != undefined) return e; } if (this.entries_.length >= 2) { if (this.entries_[0].type == EventType.SOCKET_POOL_CONNECT_JOB || this.entries_[1].type == EventType.UDP_CONNECT) { return this.entries_[1]; } if (this.entries_[0].type == EventType.REQUEST_ALIVE && this.entries_[0].params == undefined) { var start_index = 1; // Skip over URL_REQUEST_BLOCKED_ON_DELEGATE events for URL_REQUESTs. while (start_index + 1 < this.entries_.length && this.entries_[start_index].type == EventType.URL_REQUEST_BLOCKED_ON_DELEGATE) { ++start_index; } return this.entries_[start_index]; } if (this.entries_[1].type == EventType.IPV6_PROBE_RUNNING) return this.entries_[1]; } return this.entries_[0]; }, /** * Returns the first entry with the specified type, or undefined if not * found. */ findLogEntryByType_: function(type) { for (var i = 0; i < this.entries_.length; ++i) { if (this.entries_[i].type == type) { return this.entries_[i]; } } return undefined; }, /** * Returns the beginning of the last entry with the specified type, or * undefined if not found. */ findLastLogEntryStartByType_: function(type) { for (var i = this.entries_.length - 1; i >= 0; --i) { if (this.entries_[i].type == type) { if (this.entries_[i].phase != EventPhase.PHASE_END) return this.entries_[i]; } } return undefined; }, getLogEntries: function() { return this.entries_; }, getSourceTypeString: function() { return EventSourceTypeNames[this.entries_[0].source.type]; }, getSourceType: function() { return this.entries_[0].source.type; }, getSourceId: function() { return this.entries_[0].source.id; }, /** * Returns the largest source ID seen before this object was received. * Used only for sorting SourceEntries without a source by source ID. */ getMaxPreviousEntrySourceId: function() { return this.maxPreviousSourceId_; }, isInactive: function() { return this.isInactive_; }, isError: function() { return this.isError_; }, /** * Returns time of last event if inactive. Returns current time otherwise. */ getEndTime: function() { if (!this.isInactive_) { return timeutil.getCurrentTime(); } else { var endTicks = this.entries_[this.entries_.length - 1].time; return timeutil.convertTimeTicksToTime(endTicks); } }, /** * Returns the time between the first and last events with a matching * source ID. If source is still active, uses the current time for the * last event. */ getDuration: function() { var startTicks = this.entries_[0].time; var startTime = timeutil.convertTimeTicksToTime(startTicks); var endTime = this.getEndTime(); return endTime - startTime; }, /** * Prints descriptive text about |entries_| to a new node added to the end * of |parent|. */ printAsText: function(parent) { // The date will be undefined if not viewing a loaded log file. printLogEntriesAsText(this.entries_, parent, SourceTracker.getInstance().getPrivacyStripping(), Constants.clientInfo.numericDate); } }; return SourceEntry; })(); // Copyright (c) 2011 The Chromium Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. /** * This view consists of two nested divs. The outer one has a horizontal * scrollbar and the inner one has a height of 1 pixel and a width set to * allow an appropriate scroll range. The view reports scroll events to * a callback specified on construction. * * All this funkiness is necessary because there is no HTML scroll control. * TODO(mmenke): Consider implementing our own scrollbar directly. */ var HorizontalScrollbarView = (function() { 'use strict'; // We inherit from DivView. var superClass = DivView; /** * @constructor */ function HorizontalScrollbarView(divId, innerDivId, callback) { superClass.call(this, divId); this.callback_ = callback; this.innerDiv_ = $(innerDivId); $(divId).onscroll = this.onScroll_.bind(this); // The current range and position of the scrollbar. Because DOM updates // are asynchronous, the current state cannot be read directly from the DOM // after updating the range. this.range_ = 0; this.position_ = 0; // The DOM updates asynchronously, so sometimes we need a timer to update // the current scroll position after resizing the scrollbar. this.updatePositionTimerId_ = null; } HorizontalScrollbarView.prototype = { // Inherit the superclass's methods. __proto__: superClass.prototype, setGeometry: function(left, top, width, height) { superClass.prototype.setGeometry.call(this, left, top, width, height); this.setRange(this.range_); }, show: function(isVisible) { superClass.prototype.show.call(this, isVisible); }, /** * Sets the range of the scrollbar. The scrollbar can have a value * anywhere from 0 to |range|, inclusive. The width of the drag area * on the scrollbar will generally be based on the width of the scrollbar * relative to the size of |range|, so if the scrollbar is about the size * of the thing we're scrolling, we get fairly nice behavior. * * If |range| is less than the original position, |position_| is set to * |range|. Otherwise, it is not modified. */ setRange: function(range) { this.range_ = range; setNodeWidth(this.innerDiv_, this.getWidth() + range); if (range < this.position_) this.position_ = range; this.setPosition(this.position_); }, /** * Sets the position of the scrollbar. |position| must be between 0 and * |range_|, inclusive. */ setPosition: function(position) { this.position_ = position; this.updatePosition_(); }, /** * Updates the visible position of the scrollbar to be |position_|. * On failure, calls itself again after a timeout. This is needed because * setRange does not synchronously update the DOM. */ updatePosition_: function() { // Clear the timer if we have one, so we don't have two timers running at // once. This is safe even if we were just called from the timer, in // which case clearTimeout will silently fail. if (this.updatePositionTimerId_ !== null) { window.clearTimeout(this.updatePositionTimerId_); this.updatePositionTimerId_ = null; } this.getNode().scrollLeft = this.position_; if (this.getNode().scrollLeft != this.position_) { this.updatePositionTimerId_ = window.setTimeout(this.updatePosition_.bind(this)); } }, getRange: function() { return this.range_; }, getPosition: function() { return this.position_; }, onScroll_: function() { // If we're waiting to update the range, ignore messages from the // scrollbar. if (this.updatePositionTimerId_ !== null) return; var newPosition = this.getNode().scrollLeft; if (newPosition == this.position_) return; this.position_ = newPosition; this.callback_(); } }; return HorizontalScrollbarView; })(); // Copyright (c) 2011 The Chromium Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. var TopMidBottomView = (function() { 'use strict'; // We inherit from View. var superClass = View; /** * This view stacks three boxes -- one at the top, one at the bottom, and * one that fills the remaining space between those two. Either the top * or the bottom bar may be null. * * +----------------------+ * | topbar | * +----------------------+ * | | * | | * | | * | | * | middlebox | * | | * | | * | | * | | * | | * +----------------------+ * | bottombar | * +----------------------+ * * @constructor */ function TopMidBottomView(topView, midView, bottomView) { superClass.call(this); this.topView_ = topView; this.midView_ = midView; this.bottomView_ = bottomView; } TopMidBottomView.prototype = { // Inherit the superclass's methods. __proto__: superClass.prototype, setGeometry: function(left, top, width, height) { superClass.prototype.setGeometry.call(this, left, top, width, height); // Calculate the vertical split points. var topbarHeight = 0; if (this.topView_) topbarHeight = this.topView_.getHeight(); var bottombarHeight = 0; if (this.bottomView_) bottombarHeight = this.bottomView_.getHeight(); var middleboxHeight = height - (topbarHeight + bottombarHeight); if (middleboxHeight < 0) middleboxHeight = 0; // Position the boxes using calculated split points. if (this.topView_) this.topView_.setGeometry(left, top, width, topbarHeight); this.midView_.setGeometry(left, top + topbarHeight, width, middleboxHeight); if (this.bottomView_) { this.bottomView_.setGeometry(left, top + topbarHeight + middleboxHeight, width, bottombarHeight); } }, show: function(isVisible) { superClass.prototype.show.call(this, isVisible); if (this.topView_) this.topView_.show(isVisible); this.midView_.show(isVisible); if (this.bottomView_) this.bottomView_.show(isVisible); } }; return TopMidBottomView; })(); // Copyright (c) 2012 The Chromium Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. /** * Different data types that each require their own labelled axis. */ var TimelineDataType = { SOURCE_COUNT: 0, BYTES_PER_SECOND: 1 }; /** * A TimelineDataSeries collects an ordered series of (time, value) pairs, * and converts them to graph points. It also keeps track of its color and * current visibility state. DataSeries are solely responsible for tracking * data, and do not send notifications on state changes. * * Abstract class, doesn't implement onReceivedLogEntry. */ var TimelineDataSeries = (function() { 'use strict'; /** * @constructor */ function TimelineDataSeries(dataType) { // List of DataPoints in chronological order. this.dataPoints_ = []; // Data type of the DataSeries. This is used to scale all values with // the same units in the same way. this.dataType_ = dataType; // Default color. Should always be overridden prior to display. this.color_ = 'red'; // Whether or not the data series should be drawn. this.isVisible_ = false; this.cacheStartTime_ = null; this.cacheStepSize_ = 0; this.cacheValues_ = []; } TimelineDataSeries.prototype = { /** * Adds a DataPoint to |this| with the specified time and value. * DataPoints are assumed to be received in chronological order. */ addPoint: function(timeTicks, value) { var time = timeutil.convertTimeTicksToDate(timeTicks).getTime(); this.dataPoints_.push(new DataPoint(time, value)); }, isVisible: function() { return this.isVisible_; }, show: function(isVisible) { this.isVisible_ = isVisible; }, getColor: function() { return this.color_; }, setColor: function(color) { this.color_ = color; }, getDataType: function() { return this.dataType_; }, /** * Returns a list containing the values of the data series at |count| * points, starting at |startTime|, and |stepSize| milliseconds apart. * Caches values, so showing/hiding individual data series is fast, and * derived data series can be efficiently computed, if we add any. */ getValues: function(startTime, stepSize, count) { // Use cached values, if we can. if (this.cacheStartTime_ == startTime && this.cacheStepSize_ == stepSize && this.cacheValues_.length == count) { return this.cacheValues_; } // Do all the work. this.cacheValues_ = this.getValuesInternal_(startTime, stepSize, count); this.cacheStartTime_ = startTime; this.cacheStepSize_ = stepSize; return this.cacheValues_; }, /** * Does all the work of getValues when we can't use cached data. * * The default implementation just uses the |value| of the most recently * seen DataPoint before each time, but other DataSeries may use some * form of interpolation. * TODO(mmenke): Consider returning the maximum value over each interval * to create graphs more stable with respect to zooming. */ getValuesInternal_: function(startTime, stepSize, count) { var values = []; var nextPoint = 0; var currentValue = 0; var time = startTime; for (var i = 0; i < count; ++i) { while (nextPoint < this.dataPoints_.length && this.dataPoints_[nextPoint].time < time) { currentValue = this.dataPoints_[nextPoint].value; ++nextPoint; } values[i] = currentValue; time += stepSize; } return values; } }; /** * A single point in a data series. Each point has a time, in the form of * milliseconds since the Unix epoch, and a numeric value. * @constructor */ function DataPoint(time, value) { this.time = time; this.value = value; } return TimelineDataSeries; })(); /** * Tracks how many sources of the given type have seen a begin * event of type |eventType| more recently than an end event. */ var SourceCountDataSeries = (function() { 'use strict'; var superClass = TimelineDataSeries; /** * @constructor */ function SourceCountDataSeries(sourceType, eventType) { superClass.call(this, TimelineDataType.SOURCE_COUNT); this.sourceType_ = sourceType; this.eventType_ = eventType; // Map of sources for which we've seen a begin event more recently than an // end event. Each such source has a value of "true". All others are // undefined. this.activeSources_ = {}; // Number of entries in |activeSources_|. this.activeCount_ = 0; } SourceCountDataSeries.prototype = { // Inherit the superclass's methods. __proto__: superClass.prototype, onReceivedLogEntry: function(entry) { if (entry.source.type != this.sourceType_ || entry.type != this.eventType_) { return; } if (entry.phase == EventPhase.PHASE_BEGIN) { this.onBeginEvent(entry.source.id, entry.time); return; } if (entry.phase == EventPhase.PHASE_END) this.onEndEvent(entry.source.id, entry.time); }, /** * Called when the source with the specified id begins doing whatever we * care about. If it's not already an active source, we add it to the map * and add a data point. */ onBeginEvent: function(id, time) { if (this.activeSources_[id]) return; this.activeSources_[id] = true; ++this.activeCount_; this.addPoint(time, this.activeCount_); }, /** * Called when the source with the specified id stops doing whatever we * care about. If it's an active source, we remove it from the map and add * a data point. */ onEndEvent: function(id, time) { if (!this.activeSources_[id]) return; delete this.activeSources_[id]; --this.activeCount_; this.addPoint(time, this.activeCount_); } }; return SourceCountDataSeries; })(); /** * Tracks the number of sockets currently in use. Needs special handling of * SSL sockets, so can't just use a normal SourceCountDataSeries. */ var SocketsInUseDataSeries = (function() { 'use strict'; var superClass = SourceCountDataSeries; /** * @constructor */ function SocketsInUseDataSeries() { superClass.call(this, EventSourceType.SOCKET, EventType.SOCKET_IN_USE); } SocketsInUseDataSeries.prototype = { // Inherit the superclass's methods. __proto__: superClass.prototype, onReceivedLogEntry: function(entry) { // SSL sockets have two nested SOCKET_IN_USE events. This is needed to // mark SSL sockets as unused after SSL negotiation. if (entry.type == EventType.SSL_CONNECT && entry.phase == EventPhase.PHASE_END) { this.onEndEvent(entry.source.id, entry.time); return; } superClass.prototype.onReceivedLogEntry.call(this, entry); } }; return SocketsInUseDataSeries; })(); /** * Tracks approximate data rate using individual data transfer events. * Abstract class, doesn't implement onReceivedLogEntry. */ var TransferRateDataSeries = (function() { 'use strict'; var superClass = TimelineDataSeries; /** * @constructor */ function TransferRateDataSeries() { superClass.call(this, TimelineDataType.BYTES_PER_SECOND); } TransferRateDataSeries.prototype = { // Inherit the superclass's methods. __proto__: superClass.prototype, /** * Returns the average data rate over each interval, only taking into * account transfers that occurred within each interval. * TODO(mmenke): Do something better. */ getValuesInternal_: function(startTime, stepSize, count) { // Find the first DataPoint after |startTime| - |stepSize|. var nextPoint = 0; while (nextPoint < this.dataPoints_.length && this.dataPoints_[nextPoint].time < startTime - stepSize) { ++nextPoint; } var values = []; var time = startTime; for (var i = 0; i < count; ++i) { // Calculate total bytes transferred from |time| - |stepSize| // to |time|. We look at the transfers before |time| to give // us generally non-varying values for a given time. var transferred = 0; while (nextPoint < this.dataPoints_.length && this.dataPoints_[nextPoint].time < time) { transferred += this.dataPoints_[nextPoint].value; ++nextPoint; } // Calculate bytes per second. values[i] = 1000 * transferred / stepSize; time += stepSize; } return values; } }; return TransferRateDataSeries; })(); /** * Tracks TCP and UDP transfer rate. */ var NetworkTransferRateDataSeries = (function() { 'use strict'; var superClass = TransferRateDataSeries; /** * |tcpEvent| and |udpEvent| are the event types for data transfers using * TCP and UDP, respectively. * @constructor */ function NetworkTransferRateDataSeries(tcpEvent, udpEvent) { superClass.call(this); this.tcpEvent_ = tcpEvent; this.udpEvent_ = udpEvent; } NetworkTransferRateDataSeries.prototype = { // Inherit the superclass's methods. __proto__: superClass.prototype, onReceivedLogEntry: function(entry) { if (entry.type != this.tcpEvent_ && entry.type != this.udpEvent_) return; this.addPoint(entry.time, entry.params.byte_count); }, }; return NetworkTransferRateDataSeries; })(); /** * Tracks disk cache read or write rate. Doesn't include clearing, opening, * or dooming entries, as they don't have clear size values. */ var DiskCacheTransferRateDataSeries = (function() { 'use strict'; var superClass = TransferRateDataSeries; /** * @constructor */ function DiskCacheTransferRateDataSeries(eventType) { superClass.call(this); this.eventType_ = eventType; } DiskCacheTransferRateDataSeries.prototype = { // Inherit the superclass's methods. __proto__: superClass.prototype, onReceivedLogEntry: function(entry) { if (entry.source.type != EventSourceType.DISK_CACHE_ENTRY || entry.type != this.eventType_ || entry.phase != EventPhase.PHASE_END) { return; } // The disk cache has a lot of 0-length writes, when truncating entries. // Ignore those. if (entry.params.bytes_copied != 0) this.addPoint(entry.time, entry.params.bytes_copied); } }; return DiskCacheTransferRateDataSeries; })(); // Copyright (c) 2012 The Chromium Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. /** * A TimelineGraphView displays a timeline graph on a canvas element. */ var TimelineGraphView = (function() { 'use strict'; // We inherit from TopMidBottomView. var superClass = TopMidBottomView; // Default starting scale factor, in terms of milliseconds per pixel. var DEFAULT_SCALE = 1000; // Maximum number of labels placed vertically along the sides of the graph. var MAX_VERTICAL_LABELS = 6; // Vertical spacing between labels and between the graph and labels. var LABEL_VERTICAL_SPACING = 4; // Horizontal spacing between vertically placed labels and the edges of the // graph. var LABEL_HORIZONTAL_SPACING = 3; // Horizintal spacing between two horitonally placed labels along the bottom // of the graph. var LABEL_LABEL_HORIZONTAL_SPACING = 25; // Length of ticks, in pixels, next to y-axis labels. The x-axis only has // one set of labels, so it can use lines instead. var Y_AXIS_TICK_LENGTH = 10; // The number of units mouse wheel deltas increase for each tick of the // wheel. var MOUSE_WHEEL_UNITS_PER_CLICK = 120; // Amount we zoom for one vertical tick of the mouse wheel, as a ratio. var MOUSE_WHEEL_ZOOM_RATE = 1.25; // Amount we scroll for one horizontal tick of the mouse wheel, in pixels. var MOUSE_WHEEL_SCROLL_RATE = MOUSE_WHEEL_UNITS_PER_CLICK; // Number of pixels to scroll per pixel the mouse is dragged. var MOUSE_WHEEL_DRAG_RATE = 3; var GRID_COLOR = '#CCC'; var TEXT_COLOR = '#000'; var BACKGROUND_COLOR = '#FFF'; // Which side of the canvas y-axis labels should go on, for a given Graph. // TODO(mmenke): Figure out a reasonable way to handle more than 2 sets // of labels. var LabelAlign = { LEFT: 0, RIGHT: 1 }; /** * @constructor */ function TimelineGraphView(divId, canvasId, scrollbarId, scrollbarInnerId) { this.scrollbar_ = new HorizontalScrollbarView(scrollbarId, scrollbarInnerId, this.onScroll_.bind(this)); // Call superclass's constructor. superClass.call(this, null, new DivView(divId), this.scrollbar_); this.graphDiv_ = $(divId); this.canvas_ = $(canvasId); this.canvas_.onmousewheel = this.onMouseWheel_.bind(this); this.canvas_.onmousedown = this.onMouseDown_.bind(this); this.canvas_.onmousemove = this.onMouseMove_.bind(this); this.canvas_.onmouseup = this.onMouseUp_.bind(this); this.canvas_.onmouseout = this.onMouseUp_.bind(this); // Used for click and drag scrolling of graph. Drag-zooming not supported, // for a more stable scrolling experience. this.isDragging_ = false; this.dragX_ = 0; // Set the range and scale of the graph. Times are in milliseconds since // the Unix epoch. // All measurements we have must be after this time. this.startTime_ = 0; // The current rightmost position of the graph is always at most this. // We may have some later events. When actively capturing new events, it's // updated on a timer. this.endTime_ = 1; // Current scale, in terms of milliseconds per pixel. Each column of // pixels represents a point in time |scale_| milliseconds after the // previous one. We only display times that are of the form // |startTime_| + K * |scale_| to avoid jittering, and the rightmost // pixel that we can display has a time <= |endTime_|. Non-integer values // are allowed. this.scale_ = DEFAULT_SCALE; this.graphs_ = []; // Initialize the scrollbar. this.updateScrollbarRange_(true); } // Smallest allowed scaling factor. TimelineGraphView.MIN_SCALE = 5; TimelineGraphView.prototype = { // Inherit the superclass's methods. __proto__: superClass.prototype, setGeometry: function(left, top, width, height) { superClass.prototype.setGeometry.call(this, left, top, width, height); // The size of the canvas can only be set by using its |width| and // |height| properties, which do not take padding into account, so we // need to use them ourselves. var style = getComputedStyle(this.canvas_); var horizontalPadding = parseInt(style.paddingRight) + parseInt(style.paddingLeft); var verticalPadding = parseInt(style.paddingTop) + parseInt(style.paddingBottom); var canvasWidth = parseInt(this.graphDiv_.style.width) - horizontalPadding; // For unknown reasons, there's an extra 3 pixels border between the // bottom of the canvas and the bottom margin of the enclosing div. var canvasHeight = parseInt(this.graphDiv_.style.height) - verticalPadding - 3; // Protect against degenerates. if (canvasWidth < 10) canvasWidth = 10; if (canvasHeight < 10) canvasHeight = 10; this.canvas_.width = canvasWidth; this.canvas_.height = canvasHeight; // Use the same font style for the canvas as we use elsewhere. // Has to be updated every resize. this.canvas_.getContext('2d').font = getComputedStyle(this.canvas_).font; this.updateScrollbarRange_(this.graphScrolledToRightEdge_()); this.repaint(); }, show: function(isVisible) { superClass.prototype.show.call(this, isVisible); if (isVisible) this.repaint(); }, // Returns the total length of the graph, in pixels. getLength_: function() { var timeRange = this.endTime_ - this.startTime_; // Math.floor is used to ignore the last partial area, of length less // than |scale_|. return Math.floor(timeRange / this.scale_); }, /** * Returns true if the graph is scrolled all the way to the right. */ graphScrolledToRightEdge_: function() { return this.scrollbar_.getPosition() == this.scrollbar_.getRange(); }, /** * Update the range of the scrollbar. If |resetPosition| is true, also * sets the slider to point at the rightmost position and triggers a * repaint. */ updateScrollbarRange_: function(resetPosition) { var scrollbarRange = this.getLength_() - this.canvas_.width; if (scrollbarRange < 0) scrollbarRange = 0; // If we've decreased the range to less than the current scroll position, // we need to move the scroll position. if (this.scrollbar_.getPosition() > scrollbarRange) resetPosition = true; this.scrollbar_.setRange(scrollbarRange); if (resetPosition) { this.scrollbar_.setPosition(scrollbarRange); this.repaint(); } }, /** * Sets the date range displayed on the graph, switches to the default * scale factor, and moves the scrollbar all the way to the right. */ setDateRange: function(startDate, endDate) { this.startTime_ = startDate.getTime(); this.endTime_ = endDate.getTime(); // Safety check. if (this.endTime_ <= this.startTime_) this.startTime_ = this.endTime_ - 1; this.scale_ = DEFAULT_SCALE; this.updateScrollbarRange_(true); }, /** * Updates the end time at the right of the graph to be the current time. * Specifically, updates the scrollbar's range, and if the scrollbar is * all the way to the right, keeps it all the way to the right. Otherwise, * leaves the view as-is and doesn't redraw anything. */ updateEndDate: function() { this.endTime_ = timeutil.getCurrentTime(); this.updateScrollbarRange_(this.graphScrolledToRightEdge_()); }, getStartDate: function() { return new Date(this.startTime_); }, /** * Scrolls the graph horizontally by the specified amount. */ horizontalScroll_: function(delta) { var newPosition = this.scrollbar_.getPosition() + Math.round(delta); // Make sure the new position is in the right range. if (newPosition < 0) { newPosition = 0; } else if (newPosition > this.scrollbar_.getRange()) { newPosition = this.scrollbar_.getRange(); } if (this.scrollbar_.getPosition() == newPosition) return; this.scrollbar_.setPosition(newPosition); this.onScroll_(); }, /** * Zooms the graph by the specified amount. */ zoom_: function(ratio) { var oldScale = this.scale_; this.scale_ *= ratio; if (this.scale_ < TimelineGraphView.MIN_SCALE) this.scale_ = TimelineGraphView.MIN_SCALE; if (this.scale_ == oldScale) return; // If we were at the end of the range before, remain at the end of the // range. if (this.graphScrolledToRightEdge_()) { this.updateScrollbarRange_(true); return; } // Otherwise, do our best to maintain the old position. We use the // position at the far right of the graph for consistency. var oldMaxTime = oldScale * (this.scrollbar_.getPosition() + this.canvas_.width); var newMaxTime = Math.round(oldMaxTime / this.scale_); var newPosition = newMaxTime - this.canvas_.width; // Update range and scroll position. this.updateScrollbarRange_(false); this.horizontalScroll_(newPosition - this.scrollbar_.getPosition()); }, onMouseWheel_: function(event) { event.preventDefault(); this.horizontalScroll_( MOUSE_WHEEL_SCROLL_RATE * -event.wheelDeltaX / MOUSE_WHEEL_UNITS_PER_CLICK); this.zoom_(Math.pow(MOUSE_WHEEL_ZOOM_RATE, -event.wheelDeltaY / MOUSE_WHEEL_UNITS_PER_CLICK)); }, onMouseDown_: function(event) { event.preventDefault(); this.isDragging_ = true; this.dragX_ = event.clientX; }, onMouseMove_: function(event) { if (!this.isDragging_) return; event.preventDefault(); this.horizontalScroll_( MOUSE_WHEEL_DRAG_RATE * (event.clientX - this.dragX_)); this.dragX_ = event.clientX; }, onMouseUp_: function(event) { this.isDragging_ = false; }, onScroll_: function() { this.repaint(); }, /** * Replaces the current TimelineDataSeries with |dataSeries|. */ setDataSeries: function(dataSeries) { // Simplest just to recreate the Graphs. this.graphs_ = []; this.graphs_[TimelineDataType.BYTES_PER_SECOND] = new Graph(TimelineDataType.BYTES_PER_SECOND, LabelAlign.RIGHT); this.graphs_[TimelineDataType.SOURCE_COUNT] = new Graph(TimelineDataType.SOURCE_COUNT, LabelAlign.LEFT); for (var i = 0; i < dataSeries.length; ++i) this.graphs_[dataSeries[i].getDataType()].addDataSeries(dataSeries[i]); this.repaint(); }, /** * Draws the graph on |canvas_|. */ repaint: function() { this.repaintTimerRunning_ = false; if (!this.isVisible()) return; var width = this.canvas_.width; var height = this.canvas_.height; var context = this.canvas_.getContext('2d'); // Clear the canvas. context.fillStyle = BACKGROUND_COLOR; context.fillRect(0, 0, width, height); // Try to get font height in pixels. Needed for layout. var fontHeightString = context.font.match(/([0-9]+)px/)[1]; var fontHeight = parseInt(fontHeightString); // Safety check, to avoid drawing anything too ugly. if (fontHeightString.length == 0 || fontHeight <= 0 || fontHeight * 4 > height || width < 50) { return; } // Save current transformation matrix so we can restore it later. context.save(); // The center of an HTML canvas pixel is technically at (0.5, 0.5). This // makes near straight lines look bad, due to anti-aliasing. This // translation reduces the problem a little. context.translate(0.5, 0.5); // Figure out what time values to display. var position = this.scrollbar_.getPosition(); // If the entire time range is being displayed, align the right edge of // the graph to the end of the time range. if (this.scrollbar_.getRange() == 0) position = this.getLength_() - this.canvas_.width; var visibleStartTime = this.startTime_ + position * this.scale_; // Make space at the bottom of the graph for the time labels, and then // draw the labels. var textHeight = height; height -= fontHeight + LABEL_VERTICAL_SPACING; this.drawTimeLabels(context, width, height, textHeight, visibleStartTime); // Draw outline of the main graph area. context.strokeStyle = GRID_COLOR; context.strokeRect(0, 0, width - 1, height - 1); // Layout graphs and have them draw their tick marks. for (var i = 0; i < this.graphs_.length; ++i) { this.graphs_[i].layout(width, height, fontHeight, visibleStartTime, this.scale_); this.graphs_[i].drawTicks(context); } // Draw the lines of all graphs, and then draw their labels. for (var i = 0; i < this.graphs_.length; ++i) this.graphs_[i].drawLines(context); for (var i = 0; i < this.graphs_.length; ++i) this.graphs_[i].drawLabels(context); // Restore original transformation matrix. context.restore(); }, /** * Draw time labels below the graph. Takes in start time as an argument * since it may not be |startTime_|, when we're displaying the entire * time range. */ drawTimeLabels: function(context, width, height, textHeight, startTime) { // Text for a time string to use in determining how far apart // to place text labels. var sampleText = (new Date(startTime)).toLocaleTimeString(); // The desired spacing for text labels. var targetSpacing = context.measureText(sampleText).width + LABEL_LABEL_HORIZONTAL_SPACING; // The allowed time step values between adjacent labels. Anything much // over a couple minutes isn't terribly realistic, given how much memory // we use, and how slow a lot of the net-internals code is. var timeStepValues = [ 1000, // 1 second 1000 * 5, 1000 * 30, 1000 * 60, // 1 minute 1000 * 60 * 5, 1000 * 60 * 30, 1000 * 60 * 60, // 1 hour 1000 * 60 * 60 * 5 ]; // Find smallest time step value that gives us at least |targetSpacing|, // if any. var timeStep = null; for (var i = 0; i < timeStepValues.length; ++i) { if (timeStepValues[i] / this.scale_ >= targetSpacing) { timeStep = timeStepValues[i]; break; } } // If no such value, give up. if (!timeStep) return; // Find the time for the first label. This time is a perfect multiple of // timeStep because of how UTC times work. var time = Math.ceil(startTime / timeStep) * timeStep; context.textBaseline = 'bottom'; context.textAlign = 'center'; context.fillStyle = TEXT_COLOR; context.strokeStyle = GRID_COLOR; // Draw labels and vertical grid lines. while (true) { var x = Math.round((time - startTime) / this.scale_); if (x >= width) break; var text = (new Date(time)).toLocaleTimeString(); context.fillText(text, x, textHeight); context.beginPath(); context.lineTo(x, 0); context.lineTo(x, height); context.stroke(); time += timeStep; } } }; /** * A Graph is responsible for drawing all the TimelineDataSeries that have * the same data type. Graphs are responsible for scaling the values, laying * out labels, and drawing both labels and lines for its data series. */ var Graph = (function() { /** * |dataType| is the DataType that will be shared by all its DataSeries. * |labelAlign| is the LabelAlign value indicating whether the labels * should be aligned to the right of left of the graph. * @constructor */ function Graph(dataType, labelAlign) { this.dataType_ = dataType; this.dataSeries_ = []; this.labelAlign_ = labelAlign; // Cached properties of the graph, set in layout. this.width_ = 0; this.height_ = 0; this.fontHeight_ = 0; this.startTime_ = 0; this.scale_ = 0; // At least the highest value in the displayed range of the graph. // Used for scaling and setting labels. Set in layoutLabels. this.max_ = 0; // Cached text of equally spaced labels. Set in layoutLabels. this.labels_ = []; } /** * A Label is the label at a particular position along the y-axis. * @constructor */ function Label(height, text) { this.height = height; this.text = text; } Graph.prototype = { addDataSeries: function(dataSeries) { this.dataSeries_.push(dataSeries); }, /** * Returns a list of all the values that should be displayed for a given * data series, using the current graph layout. */ getValues: function(dataSeries) { if (!dataSeries.isVisible()) return null; return dataSeries.getValues(this.startTime_, this.scale_, this.width_); }, /** * Updates the graph's layout. In particular, both the max value and * label positions are updated. Must be called before calling any of the * drawing functions. */ layout: function(width, height, fontHeight, startTime, scale) { this.width_ = width; this.height_ = height; this.fontHeight_ = fontHeight; this.startTime_ = startTime; this.scale_ = scale; // Find largest value. var max = 0; for (var i = 0; i < this.dataSeries_.length; ++i) { var values = this.getValues(this.dataSeries_[i]); if (!values) continue; for (var j = 0; j < values.length; ++j) { if (values[j] > max) max = values[j]; } } this.layoutLabels_(max); }, /** * Lays out labels and sets |max_|, taking the time units into * consideration. |maxValue| is the actual maximum value, and * |max_| will be set to the value of the largest label, which * will be at least |maxValue|. */ layoutLabels_: function(maxValue) { if (this.dataType_ != TimelineDataType.BYTES_PER_SECOND) { this.layoutLabelsBasic_(maxValue, 0); return; } // Special handling for data rates. // Find appropriate units to use. var units = ['B/s', 'kB/s', 'MB/s', 'GB/s', 'TB/s', 'PB/s']; // Units to use for labels. 0 is bytes, 1 is kilobytes, etc. // We start with kilobytes, and work our way up. var unit = 1; // Update |maxValue| to be in the right units. maxValue = maxValue / 1024; while (units[unit + 1] && maxValue >= 999) { maxValue /= 1024; ++unit; } // Calculate labels. this.layoutLabelsBasic_(maxValue, 1); // Append units to labels. for (var i = 0; i < this.labels_.length; ++i) this.labels_[i] += ' ' + units[unit]; // Convert |max_| back to bytes, so it can be used when scaling values // for display. this.max_ *= Math.pow(1024, unit); }, /** * Same as layoutLabels_, but ignores units. |maxDecimalDigits| is the * maximum number of decimal digits allowed. The minimum allowed * difference between two adjacent labels is 10^-|maxDecimalDigits|. */ layoutLabelsBasic_: function(maxValue, maxDecimalDigits) { this.labels_ = []; // No labels if |maxValue| is 0. if (maxValue == 0) { this.max_ = maxValue; return; } // The maximum number of equally spaced labels allowed. |fontHeight_| // is doubled because the top two labels are both drawn in the same // gap. var minLabelSpacing = 2 * this.fontHeight_ + LABEL_VERTICAL_SPACING; // The + 1 is for the top label. var maxLabels = 1 + this.height_ / minLabelSpacing; if (maxLabels < 2) { maxLabels = 2; } else if (maxLabels > MAX_VERTICAL_LABELS) { maxLabels = MAX_VERTICAL_LABELS; } // Initial try for step size between conecutive labels. var stepSize = Math.pow(10, -maxDecimalDigits); // Number of digits to the right of the decimal of |stepSize|. // Used for formating label strings. var stepSizeDecimalDigits = maxDecimalDigits; // Pick a reasonable step size. while (true) { // If we use a step size of |stepSize| between labels, we'll need: // // Math.ceil(maxValue / stepSize) + 1 // // labels. The + 1 is because we need labels at both at 0 and at // the top of the graph. // Check if we can use steps of size |stepSize|. if (Math.ceil(maxValue / stepSize) + 1 <= maxLabels) break; // Check |stepSize| * 2. if (Math.ceil(maxValue / (stepSize * 2)) + 1 <= maxLabels) { stepSize *= 2; break; } // Check |stepSize| * 5. if (Math.ceil(maxValue / (stepSize * 5)) + 1 <= maxLabels) { stepSize *= 5; break; } stepSize *= 10; if (stepSizeDecimalDigits > 0) --stepSizeDecimalDigits; } // Set the max so it's an exact multiple of the chosen step size. this.max_ = Math.ceil(maxValue / stepSize) * stepSize; // Create labels. for (var label = this.max_; label >= 0; label -= stepSize) this.labels_.push(label.toFixed(stepSizeDecimalDigits)); }, /** * Draws tick marks for each of the labels in |labels_|. */ drawTicks: function(context) { var x1; var x2; if (this.labelAlign_ == LabelAlign.RIGHT) { x1 = this.width_ - 1; x2 = this.width_ - 1 - Y_AXIS_TICK_LENGTH; } else { x1 = 0; x2 = Y_AXIS_TICK_LENGTH; } context.fillStyle = GRID_COLOR; context.beginPath(); for (var i = 1; i < this.labels_.length - 1; ++i) { // The rounding is needed to avoid ugly 2-pixel wide anti-aliased // lines. var y = Math.round(this.height_ * i / (this.labels_.length - 1)); context.moveTo(x1, y); context.lineTo(x2, y); } context.stroke(); }, /** * Draws a graph line for each of the data series. */ drawLines: function(context) { // Factor by which to scale all values to convert them to a number from // 0 to height - 1. var scale = 0; var bottom = this.height_ - 1; if (this.max_) scale = bottom / this.max_; // Draw in reverse order, so earlier data series are drawn on top of // subsequent ones. for (var i = this.dataSeries_.length - 1; i >= 0; --i) { var values = this.getValues(this.dataSeries_[i]); if (!values) continue; context.strokeStyle = this.dataSeries_[i].getColor(); context.beginPath(); for (var x = 0; x < values.length; ++x) { // The rounding is needed to avoid ugly 2-pixel wide anti-aliased // horizontal lines. context.lineTo(x, bottom - Math.round(values[x] * scale)); } context.stroke(); } }, /** * Draw labels in |labels_|. */ drawLabels: function(context) { if (this.labels_.length == 0) return; var x; if (this.labelAlign_ == LabelAlign.RIGHT) { x = this.width_ - LABEL_HORIZONTAL_SPACING; } else { // Find the width of the widest label. var maxTextWidth = 0; for (var i = 0; i < this.labels_.length; ++i) { var textWidth = context.measureText(this.labels_[i]).width; if (maxTextWidth < textWidth) maxTextWidth = textWidth; } x = maxTextWidth + LABEL_HORIZONTAL_SPACING; } // Set up the context. context.fillStyle = TEXT_COLOR; context.textAlign = 'right'; // Draw top label, which is the only one that appears below its tick // mark. context.textBaseline = 'top'; context.fillText(this.labels_[0], x, 0); // Draw all the other labels. context.textBaseline = 'bottom'; var step = (this.height_ - 1) / (this.labels_.length - 1); for (var i = 1; i < this.labels_.length; ++i) context.fillText(this.labels_[i], x, step * i); } }; return Graph; })(); return TimelineGraphView; })(); // Copyright (c) 2012 The Chromium Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. /** * TimelineView displays a zoomable and scrollable graph of a number of values * over time. The TimelineView class itself is responsible primarily for * updating the TimelineDataSeries its GraphView displays. */ var TimelineView = (function() { 'use strict'; // We inherit from HorizontalSplitView. var superClass = HorizontalSplitView; /** * @constructor */ function TimelineView() { assertFirstConstructorCall(TimelineView); this.graphView_ = new TimelineGraphView( TimelineView.GRAPH_DIV_ID, TimelineView.GRAPH_CANVAS_ID, TimelineView.SCROLLBAR_DIV_ID, TimelineView.SCROLLBAR_INNER_DIV_ID); // Call superclass's constructor. var selectionView = new DivView(TimelineView.SELECTION_DIV_ID); superClass.call(this, selectionView, this.graphView_); this.selectionDivFullWidth_ = selectionView.getWidth(); $(TimelineView.SELECTION_TOGGLE_ID).onclick = this.toggleSelectionDiv_.bind(this); // Interval id returned by window.setInterval for update timer. this.updateIntervalId_ = null; // List of DataSeries. These are shared with the TimelineGraphView. The // TimelineView updates their state, the TimelineGraphView reads their // state and draws them. this.dataSeries_ = []; // DataSeries depend on some of the global constants, so they're only // created once constants have been received. We also use this message to // recreate DataSeries when log files are being loaded. g_browser.addConstantsObserver(this); // We observe new log entries to determine the range of the graph, and pass // them on to each DataSource. We initialize the graph range to initially // include all events, but after that, we only update it to be the current // time on a timer. EventsTracker.getInstance().addLogEntryObserver(this); this.graphRangeInitialized_ = false; } // ID for special HTML element in category_tabs.html TimelineView.TAB_HANDLE_ID = 'tab-handle-timeline'; // IDs for special HTML elements in timeline_view.html TimelineView.GRAPH_DIV_ID = 'timeline-view-graph-div'; TimelineView.GRAPH_CANVAS_ID = 'timeline-view-graph-canvas'; TimelineView.SELECTION_DIV_ID = 'timeline-view-selection-div'; TimelineView.SELECTION_TOGGLE_ID = 'timeline-view-selection-toggle'; TimelineView.SELECTION_UL_ID = 'timeline-view-selection-ul'; TimelineView.SCROLLBAR_DIV_ID = 'timeline-view-scrollbar-div'; TimelineView.SCROLLBAR_INNER_DIV_ID = 'timeline-view-scrollbar-inner-div'; TimelineView.OPEN_SOCKETS_ID = 'timeline-view-open-sockets'; TimelineView.IN_USE_SOCKETS_ID = 'timeline-view-in-use-sockets'; TimelineView.URL_REQUESTS_ID = 'timeline-view-url-requests'; TimelineView.DNS_REQUESTS_ID = 'timeline-view-dns-requests'; TimelineView.DNS_JOBS_ID = 'timeline-view-dns-jobs'; TimelineView.BYTES_RECEIVED_ID = 'timeline-view-bytes-received'; TimelineView.BYTES_SENT_ID = 'timeline-view-bytes-sent'; TimelineView.DISK_CACHE_BYTES_READ_ID = 'timeline-view-disk-cache-bytes-read'; TimelineView.DISK_CACHE_BYTES_WRITTEN_ID = 'timeline-view-disk-cache-bytes-written'; // Class used for hiding the colored squares next to the labels for the // lines. TimelineView.HIDDEN_CLASS = 'timeline-view-hidden'; cr.addSingletonGetter(TimelineView); // Frequency with which we increase update the end date to be the current // time, when actively capturing events. var UPDATE_INTERVAL_MS = 2000; TimelineView.prototype = { // Inherit the superclass's methods. __proto__: superClass.prototype, setGeometry: function(left, top, width, height) { superClass.prototype.setGeometry.call(this, left, top, width, height); }, show: function(isVisible) { superClass.prototype.show.call(this, isVisible); // If we're hidden or not capturing events, we don't want to update the // graph's range. if (!isVisible || g_browser.isDisabled()) { this.setUpdateEndDateInterval_(0); return; } // Otherwise, update the visible range on a timer. this.setUpdateEndDateInterval_(UPDATE_INTERVAL_MS); this.updateEndDate_(); }, /** * Starts calling the GraphView's updateEndDate function every |intervalMs| * milliseconds. If |intervalMs| is 0, stops calling the function. */ setUpdateEndDateInterval_: function(intervalMs) { if (this.updateIntervalId_ !== null) { window.clearInterval(this.updateIntervalId_); this.updateIntervalId_ = null; } if (intervalMs > 0) { this.updateIntervalId_ = window.setInterval(this.updateEndDate_.bind(this), intervalMs); } }, /** * Updates the end date of graph to be the current time, unless the * BrowserBridge is disabled. */ updateEndDate_: function() { // If we loaded a log file or capturing data was stopped, stop the timer. if (g_browser.isDisabled()) { this.setUpdateEndDateInterval_(0); return; } this.graphView_.updateEndDate(); }, onLoadLogFinish: function(data) { this.setUpdateEndDateInterval_(0); return true; }, /** * Updates the visibility state of |dataSeries| to correspond to the * current checked state of |checkBox|. Also updates the class of * |listItem| based on the new visibility state. */ updateDataSeriesVisibility_: function(dataSeries, listItem, checkBox) { dataSeries.show(checkBox.checked); if (checkBox.checked) listItem.classList.remove(TimelineView.HIDDEN_CLASS); else listItem.classList.add(TimelineView.HIDDEN_CLASS); }, dataSeriesClicked_: function(dataSeries, listItem, checkBox) { this.updateDataSeriesVisibility_(dataSeries, listItem, checkBox); this.graphView_.repaint(); }, /** * Adds the specified DataSeries to |dataSeries_|, and hooks up * |listItemId|'s checkbox and color to correspond to the current state * of the given DataSeries. */ addDataSeries_: function(dataSeries, listItemId) { this.dataSeries_.push(dataSeries); var listItem = $(listItemId); var checkBox = $(listItemId).querySelector('input'); // Make sure |listItem| is visible, and then use its color for the // DataSource. listItem.classList.remove(TimelineView.HIDDEN_CLASS); dataSeries.setColor(getComputedStyle(listItem).color); this.updateDataSeriesVisibility_(dataSeries, listItem, checkBox); checkBox.onclick = this.dataSeriesClicked_.bind(this, dataSeries, listItem, checkBox); }, /** * Recreate all DataSeries. Global constants must have been set before * this is called. */ createDataSeries_: function() { this.graphRangeInitialized_ = false; this.dataSeries_ = []; this.addDataSeries_(new SourceCountDataSeries( EventSourceType.SOCKET, EventType.SOCKET_ALIVE), TimelineView.OPEN_SOCKETS_ID); this.addDataSeries_(new SocketsInUseDataSeries(), TimelineView.IN_USE_SOCKETS_ID); this.addDataSeries_(new SourceCountDataSeries( EventSourceType.URL_REQUEST, EventType.REQUEST_ALIVE), TimelineView.URL_REQUESTS_ID); this.addDataSeries_(new SourceCountDataSeries( EventSourceType.HOST_RESOLVER_IMPL_REQUEST, EventType.HOST_RESOLVER_IMPL_REQUEST), TimelineView.DNS_REQUESTS_ID); this.addDataSeries_(new SourceCountDataSeries( EventSourceType.HOST_RESOLVER_IMPL_JOB, EventType.HOST_RESOLVER_IMPL_JOB), TimelineView.DNS_JOBS_ID); this.addDataSeries_(new NetworkTransferRateDataSeries( EventType.SOCKET_BYTES_RECEIVED, EventType.UDP_BYTES_RECEIVED), TimelineView.BYTES_RECEIVED_ID); this.addDataSeries_(new NetworkTransferRateDataSeries( EventType.SOCKET_BYTES_SENT, EventType.UDP_BYTES_SENT), TimelineView.BYTES_SENT_ID); this.addDataSeries_(new DiskCacheTransferRateDataSeries( EventType.ENTRY_READ_DATA), TimelineView.DISK_CACHE_BYTES_READ_ID); this.addDataSeries_(new DiskCacheTransferRateDataSeries( EventType.ENTRY_WRITE_DATA), TimelineView.DISK_CACHE_BYTES_WRITTEN_ID); this.graphView_.setDataSeries(this.dataSeries_); }, /** * When we receive the constants, create or recreate the DataSeries. */ onReceivedConstants: function(constants) { this.createDataSeries_(); }, /** * When all log entries are deleted, recreate the DataSeries. */ onAllLogEntriesDeleted: function() { this.graphRangeInitialized_ = false; this.createDataSeries_(); }, onReceivedLogEntries: function(entries) { // Pass each entry to every DataSeries, one at a time. Not having each // data series get data directly from the EventsTracker saves us from // having very un-Javascript-like destructors for when we load new, // constants and slightly simplifies DataSeries objects. for (var entry = 0; entry < entries.length; ++entry) { for (var i = 0; i < this.dataSeries_.length; ++i) this.dataSeries_[i].onReceivedLogEntry(entries[entry]); } // If this is the first non-empty set of entries we've received, or we're // viewing a loaded log file, we will need to update the date range. if (this.graphRangeInitialized_ && !MainView.isViewingLoadedLog()) return; if (entries.length == 0) return; // Update the date range. var startDate; if (!this.graphRangeInitialized_) { startDate = timeutil.convertTimeTicksToDate(entries[0].time); } else { startDate = this.graphView_.getStartDate(); } var endDate = timeutil.convertTimeTicksToDate(entries[entries.length - 1].time); this.graphView_.setDateRange(startDate, endDate); this.graphRangeInitialized_ = true; }, toggleSelectionDiv_: function() { var toggle = $(TimelineView.SELECTION_TOGGLE_ID); var shouldCollapse = toggle.className == 'timeline-view-rotateleft'; setNodeDisplay($(TimelineView.SELECTION_UL_ID), !shouldCollapse); toggle.className = shouldCollapse ? 'timeline-view-rotateright' : 'timeline-view-rotateleft'; // Figure out the appropriate width for the selection div. var newWidth; if (shouldCollapse) { newWidth = toggle.offsetWidth; } else { newWidth = this.selectionDivFullWidth_; } // Change the width on the selection view (doesn't matter what we // set the other values to, since we will re-layout in the next line). this.leftView_.setGeometry(0, 0, newWidth, 100); // Force a re-layout now that the left view has changed width. this.setGeometry(this.getLeft(), this.getTop(), this.getWidth(), this.getHeight()); } }; return TimelineView; })(); // Copyright (c) 2012 The Chromium Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. // TODO(eroman): put these methods into a namespace. var printLogEntriesAsText; var searchLogEntriesForText; var proxySettingsToString; var stripCookiesAndLoginInfo; // Start of anonymous namespace. (function() { 'use strict'; function canCollapseBeginWithEnd(beginEntry) { return beginEntry && beginEntry.isBegin() && beginEntry.end && beginEntry.end.index == beginEntry.index + 1 && (!beginEntry.orig.params || !beginEntry.end.orig.params); } /** * Adds a child pre element to the end of |parent|, and writes the * formatted contents of |logEntries| to it. */ printLogEntriesAsText = function(logEntries, parent, privacyStripping, logCreationTime) { var tablePrinter = createTablePrinter(logEntries, privacyStripping, logCreationTime); // Format the table for fixed-width text. tablePrinter.toText(0, parent); } /** * Searches the table that would be output by printLogEntriesAsText for * |searchString|. Returns true if |searchString| would appear entirely within * any field in the table. |searchString| must be lowercase. * * Seperate function from printLogEntriesAsText since TablePrinter.toText * modifies the DOM. */ searchLogEntriesForText = function(searchString, logEntries, privacyStripping) { var tablePrinter = createTablePrinter(logEntries, privacyStripping, undefined); // Format the table for fixed-width text. return tablePrinter.search(searchString); } /** * Creates a TablePrinter for use by the above two functions. */ function createTablePrinter(logEntries, privacyStripping, logCreationTime) { var entries = LogGroupEntry.createArrayFrom(logEntries); var tablePrinter = new TablePrinter(); var parameterOutputter = new ParameterOutputter(tablePrinter); if (entries.length == 0) return tablePrinter; var startTime = timeutil.convertTimeTicksToTime(entries[0].orig.time); for (var i = 0; i < entries.length; ++i) { var entry = entries[i]; // Avoid printing the END for a BEGIN that was immediately before, unless // both have extra parameters. if (!entry.isEnd() || !canCollapseBeginWithEnd(entry.begin)) { var entryTime = timeutil.convertTimeTicksToTime(entry.orig.time); addRowWithTime(tablePrinter, entryTime, startTime); for (var j = entry.getDepth(); j > 0; --j) tablePrinter.addCell(' '); var eventText = getTextForEvent(entry); // Get the elapsed time, and append it to the event text. if (entry.isBegin()) { var dt = '?'; // Definite time. if (entry.end) { dt = entry.end.orig.time - entry.orig.time; } else if (logCreationTime != undefined) { dt = (logCreationTime - entryTime) + '+'; } eventText += ' [dt=' + dt + ']'; } var mainCell = tablePrinter.addCell(eventText); mainCell.allowOverflow = true; } // Output the extra parameters. if (typeof entry.orig.params == 'object') { // Those 5 skipped cells are: two for "t=", and three for "st=". tablePrinter.setNewRowCellIndent(5 + entry.getDepth()); writeParameters(entry.orig, privacyStripping, parameterOutputter); tablePrinter.setNewRowCellIndent(0); } } // If viewing a saved log file, add row with just the time the log was // created, if the event never completed. if (logCreationTime != undefined && entries[entries.length - 1].getDepth() > 0) { addRowWithTime(tablePrinter, logCreationTime, startTime); } return tablePrinter; } /** * Adds a new row to the given TablePrinter, and adds five cells containing * information about the time an event occured. * Format is '[t=] [st=]'. * @param {TablePrinter} tablePrinter The table printer to add the cells to. * @param {number} eventTime The time the event occured, as a UTC time in * milliseconds. * @param {number} startTime The time the first event for the source occured, * as a UTC time in milliseconds. */ function addRowWithTime(tablePrinter, eventTime, startTime) { tablePrinter.addRow(); tablePrinter.addCell('t='); var tCell = tablePrinter.addCell(eventTime); tCell.alignRight = true; tablePrinter.addCell(' [st='); var stCell = tablePrinter.addCell(eventTime - startTime); stCell.alignRight = true; tablePrinter.addCell('] '); } /** * |hexString| must be a string of hexadecimal characters with no whitespace, * whose length is a multiple of two. Writes multiple lines to |out| with * the hexadecimal characters from |hexString| on the left, in groups of * two, and their corresponding ASCII characters on the right. * * |asciiCharsPerLine| specifies how many ASCII characters will be put on each * line of the output string. */ function writeHexString(hexString, asciiCharsPerLine, out) { // Number of transferred bytes in a line of output. Length of a // line is roughly 4 times larger. var hexCharsPerLine = 2 * asciiCharsPerLine; for (var i = 0; i < hexString.length; i += hexCharsPerLine) { var hexLine = ''; var asciiLine = ''; for (var j = i; j < i + hexCharsPerLine && j < hexString.length; j += 2) { var hex = hexString.substr(j, 2); hexLine += hex + ' '; var charCode = parseInt(hex, 16); // For ASCII codes 32 though 126, display the corresponding // characters. Use a space for nulls, and a period for // everything else. if (charCode >= 0x20 && charCode <= 0x7E) { asciiLine += String.fromCharCode(charCode); } else if (charCode == 0x00) { asciiLine += ' '; } else { asciiLine += '.'; } } // Make the ASCII text for the last line of output align with the previous // lines. hexLine += makeRepeatedString(' ', 3 * asciiCharsPerLine - hexLine.length); out.writeLine(' ' + hexLine + ' ' + asciiLine); } } /** * Wrapper around a TablePrinter to simplify outputting lines of text for event * parameters. */ var ParameterOutputter = (function() { /** * @constructor */ function ParameterOutputter(tablePrinter) { this.tablePrinter_ = tablePrinter; } ParameterOutputter.prototype = { /** * Outputs a single line. */ writeLine: function(line) { this.tablePrinter_.addRow(); var cell = this.tablePrinter_.addCell(line); cell.allowOverflow = true; return cell; }, /** * Outputs a key=value line which looks like: * * --> key = value */ writeArrowKeyValue: function(key, value, link) { var cell = this.writeLine(kArrow + key + ' = ' + value); cell.link = link; }, /** * Outputs a key= line which looks like: * * --> key = */ writeArrowKey: function(key) { this.writeLine(kArrow + key + ' ='); }, /** * Outputs multiple lines, each indented by numSpaces. * For instance if numSpaces=8 it might look like this: * * line 1 * line 2 * line 3 */ writeSpaceIndentedLines: function(numSpaces, lines) { var prefix = makeRepeatedString(' ', numSpaces); for (var i = 0; i < lines.length; ++i) this.writeLine(prefix + lines[i]); }, /** * Outputs multiple lines such that the first line has * an arrow pointing at it, and subsequent lines * align with the first one. For example: * * --> line 1 * line 2 * line 3 */ writeArrowIndentedLines: function(lines) { if (lines.length == 0) return; this.writeLine(kArrow + lines[0]); for (var i = 1; i < lines.length; ++i) this.writeLine(kArrowIndentation + lines[i]); } }; var kArrow = ' --> '; var kArrowIndentation = ' '; return ParameterOutputter; })(); // end of ParameterOutputter /** * Formats the parameters for |entry| and writes them to |out|. * Certain event types have custom pretty printers. Everything else will * default to a JSON-like format. */ function writeParameters(entry, privacyStripping, out) { if (privacyStripping) { // If privacy stripping is enabled, remove data as needed. entry = stripCookiesAndLoginInfo(entry); } else { // If headers are in an object, convert them to an array for better display. entry = reformatHeaders(entry); } // Use any parameter writer available for this event type. var paramsWriter = getParamaterWriterForEventType(entry.type); var consumedParams = {}; if (paramsWriter) paramsWriter(entry, out, consumedParams); // Write any un-consumed parameters. for (var k in entry.params) { if (consumedParams[k]) continue; defaultWriteParameter(k, entry.params[k], out); } } /** * Finds a writer to format the parameters for events of type |eventType|. * * @return {function} The returned function "writer" can be invoked * as |writer(entry, writer, consumedParams)|. It will * output the parameters of |entry| to |out|, and fill * |consumedParams| with the keys of the parameters * consumed. If no writer is available for |eventType| then * returns null. */ function getParamaterWriterForEventType(eventType) { switch (eventType) { case EventType.HTTP_TRANSACTION_SEND_REQUEST_HEADERS: case EventType.HTTP_TRANSACTION_SEND_TUNNEL_HEADERS: return writeParamsForRequestHeaders; case EventType.PROXY_CONFIG_CHANGED: return writeParamsForProxyConfigChanged; case EventType.CERT_VERIFIER_JOB: case EventType.SSL_CERTIFICATES_RECEIVED: return writeParamsForCertificates; case EventType.SSL_VERSION_FALLBACK: return writeParamsForSSLVersionFallback; } return null; } /** * Default parameter writer that outputs a visualization of field named |key| * with value |value| to |out|. */ function defaultWriteParameter(key, value, out) { if (key == 'headers' && value instanceof Array) { out.writeArrowIndentedLines(value); return; } // For transferred bytes, display the bytes in hex and ASCII. if (key == 'hex_encoded_bytes' && typeof value == 'string') { out.writeArrowKey(key); writeHexString(value, 20, out); return; } // Handle source_dependency entries - add link and map source type to // string. if (key == 'source_dependency' && typeof value == 'object') { var link = '#events&s=' + value.id; var valueStr = value.id + ' (' + EventSourceTypeNames[value.type] + ')'; out.writeArrowKeyValue(key, valueStr, link); return; } if (key == 'net_error' && typeof value == 'number') { var valueStr = value + ' (' + netErrorToString(value) + ')'; out.writeArrowKeyValue(key, valueStr); return; } if (key == 'load_flags' && typeof value == 'number') { var valueStr = value + ' (' + getLoadFlagSymbolicString(value) + ')'; out.writeArrowKeyValue(key, valueStr); return; } if (key == 'load_state' && typeof value == 'number') { var valueStr = value + ' (' + getKeyWithValue(LoadState, value) + ')'; out.writeArrowKeyValue(key, valueStr); return; } // Otherwise just default to JSON formatting of the value. out.writeArrowKeyValue(key, JSON.stringify(value)); } /** * Returns the set of LoadFlags that make up the integer |loadFlag|. * For example: getLoadFlagSymbolicString( */ function getLoadFlagSymbolicString(loadFlag) { // Load flag of 0 means "NORMAL". Special case this, since and-ing with // 0 is always going to be false. if (loadFlag == 0) return getKeyWithValue(LoadFlag, loadFlag); var matchingLoadFlagNames = []; for (var k in LoadFlag) { if (loadFlag & LoadFlag[k]) matchingLoadFlagNames.push(k); } return matchingLoadFlagNames.join(' | '); } /** * Converts an SSL version number to a textual representation. * For instance, SSLVersionNumberToName(0x0301) returns 'TLS 1.0'. */ function SSLVersionNumberToName(version) { if ((version & 0xFFFF) != version) { // If the version number is more than 2 bytes long something is wrong. // Print it as hex. return 'SSL 0x' + version.toString(16); } // See if it is a known TLS name. var kTLSNames = { 0x0301: 'TLS 1.0', 0x0302: 'TLS 1.1', 0x0303: 'TLS 1.2' }; var name = kTLSNames[version]; if (name) return name; // Otherwise label it as an SSL version. var major = (version & 0xFF00) >> 8; var minor = version & 0x00FF; return 'SSL ' + major + '.' + minor; } /** * TODO(eroman): get rid of this, as it is only used by 1 callsite. * * Indent |lines| by |start|. * * For example, if |start| = ' -> ' and |lines| = ['line1', 'line2', 'line3'] * the output will be: * * " -> line1\n" + * " line2\n" + * " line3" */ function indentLines(start, lines) { return start + lines.join('\n' + makeRepeatedString(' ', start.length)); } /** * If entry.param.headers exists and is an object other than an array, converts * it into an array and returns a new entry. Otherwise, just returns the * original entry. */ function reformatHeaders(entry) { // If there are no headers, or it is not an object other than an array, // return |entry| without modification. if (!entry.params || entry.params.headers === undefined || typeof entry.params.headers != 'object' || entry.params.headers instanceof Array) { return entry; } // Duplicate the top level object, and |entry.params|, so the original object // will not be modified. entry = shallowCloneObject(entry); entry.params = shallowCloneObject(entry.params); // Convert headers to an array. var headers = []; for (var key in entry.params.headers) headers.push(key + ': ' + entry.params.headers[key]); entry.params.headers = headers; return entry; } /** * Removes a cookie or unencrypted login information from a single HTTP header * line, if present, and returns the modified line. Otherwise, just returns * the original line. */ function stripCookieOrLoginInfo(line) { var patterns = [ // Cookie patterns /^set-cookie: /i, /^set-cookie2: /i, /^cookie: /i, // Unencrypted authentication patterns /^authorization: \S*\s*/i, /^proxy-authorization: \S*\s*/i]; // Prefix will hold the first part of the string that contains no private // information. If null, no part of the string contains private information. var prefix = null; for (var i = 0; i < patterns.length; i++) { var match = patterns[i].exec(line); if (match != null) { prefix = match[0]; break; } } // Look for authentication information from data received from the server in // multi-round Negotiate authentication. if (prefix === null) { var challengePatterns = [ /^www-authenticate: (\S*)\s*/i, /^proxy-authenticate: (\S*)\s*/i]; for (var i = 0; i < challengePatterns.length; i++) { var match = challengePatterns[i].exec(line); if (!match) continue; // If there's no data after the scheme name, do nothing. if (match[0].length == line.length) break; // Ignore lines with commas, as they may contain lists of schemes, and // the information we want to hide is Base64 encoded, so has no commas. if (line.indexOf(',') >= 0) break; // Ignore Basic and Digest authentication challenges, as they contain // public information. if (/^basic$/i.test(match[1]) || /^digest$/i.test(match[1])) break; prefix = match[0]; break; } } if (prefix) { var suffix = line.slice(prefix.length); // If private information has already been removed, keep the line as-is. // This is often the case when viewing a loaded log. // TODO(mmenke): Remove '[value was stripped]' check once M24 hits stable. if (suffix.search(/^\[[0-9]+ bytes were stripped\]$/) == -1 && suffix != '[value was stripped]') { return prefix + '[' + suffix.length + ' bytes were stripped]'; } } return line; } /** * If |entry| has headers, returns a copy of |entry| with all cookie and * unencrypted login text removed. Otherwise, returns original |entry| object. * This is needed so that JSON log dumps can be made without affecting the * source data. Converts headers stored in objects to arrays. */ stripCookiesAndLoginInfo = function(entry) { if (!entry.params || entry.params.headers === undefined || !(entry.params.headers instanceof Object)) { return entry; } // Make sure entry's headers are in an array. entry = reformatHeaders(entry); // Duplicate the top level object, and |entry.params|. All other fields are // just pointers to the original values, as they won't be modified, other than // |entry.params.headers|. entry = shallowCloneObject(entry); entry.params = shallowCloneObject(entry.params); entry.params.headers = entry.params.headers.map(stripCookieOrLoginInfo); return entry; } /** * Outputs the request header parameters of |entry| to |out|. */ function writeParamsForRequestHeaders(entry, out, consumedParams) { var params = entry.params; if (!(typeof params.line == 'string') || !(params.headers instanceof Array)) { // Unrecognized params. return; } // Strip the trailing CRLF that params.line contains. var lineWithoutCRLF = params.line.replace(/\r\n$/g, ''); out.writeArrowIndentedLines([lineWithoutCRLF].concat(params.headers)); consumedParams.line = true; consumedParams.headers = true; } /** * Outputs the certificate parameters of |entry| to |out|. */ function writeParamsForCertificates(entry, out, consumedParams) { if (!(entry.params.certificates instanceof Array)) { // Unrecognized params. return; } var certs = entry.params.certificates.reduce(function(previous, current) { return previous.concat(current.split('\n')); }, new Array()); out.writeArrowKey('certificates'); out.writeSpaceIndentedLines(8, certs); consumedParams.certificates = true; } /** * Outputs the SSL version fallback parameters of |entry| to |out|. */ function writeParamsForSSLVersionFallback(entry, out, consumedParams) { var params = entry.params; if (typeof params.version_before != 'number' || typeof params.version_after != 'number') { // Unrecognized params. return; } var line = SSLVersionNumberToName(params.version_before) + ' ==> ' + SSLVersionNumberToName(params.version_after); out.writeArrowIndentedLines([line]); consumedParams.version_before = true; consumedParams.version_after = true; } function writeParamsForProxyConfigChanged(entry, out, consumedParams) { var params = entry.params; if (typeof params.new_config != 'object') { // Unrecognized params. return; } if (typeof params.old_config == 'object') { var oldConfigString = proxySettingsToString(params.old_config); // The previous configuration may not be present in the case of // the initial proxy settings fetch. out.writeArrowKey('old_config'); out.writeSpaceIndentedLines(8, oldConfigString.split('\n')); consumedParams.old_config = true; } var newConfigString = proxySettingsToString(params.new_config); out.writeArrowKey('new_config'); out.writeSpaceIndentedLines(8, newConfigString.split('\n')); consumedParams.new_config = true; } function getTextForEvent(entry) { var text = ''; if (entry.isBegin() && canCollapseBeginWithEnd(entry)) { // Don't prefix with '+' if we are going to collapse the END event. text = ' '; } else if (entry.isBegin()) { text = '+' + text; } else if (entry.isEnd()) { text = '-' + text; } else { text = ' '; } text += EventTypeNames[entry.orig.type]; return text; } proxySettingsToString = function(config) { if (!config) return ''; // The proxy settings specify up to three major fallback choices // (auto-detect, custom pac url, or manual settings). // We enumerate these to a list so we can later number them. var modes = []; // Output any automatic settings. if (config.auto_detect) modes.push(['Auto-detect']); if (config.pac_url) modes.push(['PAC script: ' + config.pac_url]); // Output any manual settings. if (config.single_proxy || config.proxy_per_scheme) { var lines = []; if (config.single_proxy) { lines.push('Proxy server: ' + config.single_proxy); } else if (config.proxy_per_scheme) { for (var urlScheme in config.proxy_per_scheme) { if (urlScheme != 'fallback') { lines.push('Proxy server for ' + urlScheme.toUpperCase() + ': ' + config.proxy_per_scheme[urlScheme]); } } if (config.proxy_per_scheme.fallback) { lines.push('Proxy server for everything else: ' + config.proxy_per_scheme.fallback); } } // Output any proxy bypass rules. if (config.bypass_list) { if (config.reverse_bypass) { lines.push('Reversed bypass list: '); } else { lines.push('Bypass list: '); } for (var i = 0; i < config.bypass_list.length; ++i) lines.push(' ' + config.bypass_list[i]); } modes.push(lines); } var result = []; if (modes.length < 1) { // If we didn't find any proxy settings modes, we are using DIRECT. result.push('Use DIRECT connections.'); } else if (modes.length == 1) { // If there was just one mode, don't bother numbering it. result.push(modes[0].join('\n')); } else { // Otherwise concatenate all of the modes into a numbered list // (which correspond with the fallback order). for (var i = 0; i < modes.length; ++i) result.push(indentLines('(' + (i + 1) + ') ', modes[i])); } if (config.source != undefined && config.source != 'UNKNOWN') result.push('Source: ' + config.source); return result.join('\n'); }; // End of anonymous namespace. })(); // Copyright (c) 2012 The Chromium Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. /** * @fileoverview * LogGroupEntry is a wrapper around log entries, which makes it easier to * find the corresponding start/end of events. * * This is used internally by the log and timeline views to pretty print * collections of log entries. */ // TODO(eroman): document these methods! var LogGroupEntry = (function() { 'use strict'; function LogGroupEntry(origEntry, index) { this.orig = origEntry; this.index = index; } LogGroupEntry.prototype = { isBegin: function() { return this.orig.phase == EventPhase.PHASE_BEGIN; }, isEnd: function() { return this.orig.phase == EventPhase.PHASE_END; }, getDepth: function() { var depth = 0; var p = this.parentEntry; while (p) { depth += 1; p = p.parentEntry; } return depth; } }; function findParentIndex(parentStack, eventType) { for (var i = parentStack.length - 1; i >= 0; --i) { if (parentStack[i].orig.type == eventType) return i; } return -1; } /** * Returns a list of LogGroupEntrys. This basically wraps the original log * entry, but makes it easier to find the start/end of the event. */ LogGroupEntry.createArrayFrom = function(origEntries) { var groupedEntries = []; // Stack of enclosing PHASE_BEGIN elements. var parentStack = []; for (var i = 0; i < origEntries.length; ++i) { var origEntry = origEntries[i]; var groupEntry = new LogGroupEntry(origEntry, i); groupedEntries.push(groupEntry); // If this is the end of an event, match it to the start. if (groupEntry.isEnd()) { // Walk up the parent stack to find the corresponding BEGIN for this // END. var parentIndex = findParentIndex(parentStack, groupEntry.orig.type); if (parentIndex == -1) { // Unmatched end. } else { groupEntry.begin = parentStack[parentIndex]; // Consider this as the terminator for all open BEGINs up until // parentIndex. while (parentIndex < parentStack.length) { var p = parentStack.pop(); p.end = groupEntry; } } } // Inherit the current parent. if (parentStack.length > 0) groupEntry.parentEntry = parentStack[parentStack.length - 1]; if (groupEntry.isBegin()) parentStack.push(groupEntry); } return groupedEntries; }; return LogGroupEntry; })(); // Copyright (c) 2012 The Chromium Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. /** * This view displays information on the proxy setup: * * - Shows the current proxy settings. * - Has a button to reload these settings. * - Shows the list of proxy hostnames that are cached as "bad". * - Has a button to clear the cached bad proxies. */ var ProxyView = (function() { 'use strict'; // We inherit from DivView. var superClass = DivView; /** * @constructor */ function ProxyView() { assertFirstConstructorCall(ProxyView); // Call superclass's constructor. superClass.call(this, ProxyView.MAIN_BOX_ID); // Hook up the UI components. $(ProxyView.RELOAD_SETTINGS_BUTTON_ID).onclick = g_browser.sendReloadProxySettings.bind(g_browser); $(ProxyView.CLEAR_BAD_PROXIES_BUTTON_ID).onclick = g_browser.sendClearBadProxies.bind(g_browser); // Register to receive proxy information as it changes. g_browser.addProxySettingsObserver(this, true); g_browser.addBadProxiesObserver(this, true); } // ID for special HTML element in category_tabs.html ProxyView.TAB_HANDLE_ID = 'tab-handle-proxy'; // IDs for special HTML elements in proxy_view.html ProxyView.MAIN_BOX_ID = 'proxy-view-tab-content'; ProxyView.ORIGINAL_SETTINGS_DIV_ID = 'proxy-view-original-settings'; ProxyView.EFFECTIVE_SETTINGS_DIV_ID = 'proxy-view-effective-settings'; ProxyView.RELOAD_SETTINGS_BUTTON_ID = 'proxy-view-reload-settings'; ProxyView.BAD_PROXIES_TBODY_ID = 'proxy-view-bad-proxies-tbody'; ProxyView.CLEAR_BAD_PROXIES_BUTTON_ID = 'proxy-view-clear-bad-proxies'; cr.addSingletonGetter(ProxyView); ProxyView.prototype = { // Inherit the superclass's methods. __proto__: superClass.prototype, onLoadLogFinish: function(data) { return this.onProxySettingsChanged(data.proxySettings) && this.onBadProxiesChanged(data.badProxies); }, onProxySettingsChanged: function(proxySettings) { // Both |original| and |effective| are dictionaries describing the // settings. $(ProxyView.ORIGINAL_SETTINGS_DIV_ID).innerHTML = ''; $(ProxyView.EFFECTIVE_SETTINGS_DIV_ID).innerHTML = ''; if (!proxySettings) return false; var original = proxySettings.original; var effective = proxySettings.effective; $(ProxyView.ORIGINAL_SETTINGS_DIV_ID).innerText = proxySettingsToString(original); $(ProxyView.EFFECTIVE_SETTINGS_DIV_ID).innerText = proxySettingsToString(effective); return true; }, onBadProxiesChanged: function(badProxies) { $(ProxyView.BAD_PROXIES_TBODY_ID).innerHTML = ''; if (!badProxies) return false; // Add a table row for each bad proxy entry. for (var i = 0; i < badProxies.length; ++i) { var entry = badProxies[i]; var badUntilDate = timeutil.convertTimeTicksToDate(entry.bad_until); var tr = addNode($(ProxyView.BAD_PROXIES_TBODY_ID), 'tr'); var nameCell = addNode(tr, 'td'); var badUntilCell = addNode(tr, 'td'); addTextNode(nameCell, entry.proxy_uri); timeutil.addNodeWithDate(badUntilCell, badUntilDate); } return true; } }; return ProxyView; })(); // Copyright (c) 2011 The Chromium Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. var SocketPoolWrapper = (function() { 'use strict'; /** * SocketPoolWrapper is a wrapper around socket pools entries. It's * used by the log and sockets view to print tables containing both * a synopsis of the state of all pools, and listing the groups within * individual pools. * * The constructor takes a socket pool and its parent, and generates a * unique name from the two, which is stored as |fullName|. |parent| * must itself be a SocketPoolWrapper. * * @constructor */ function SocketPoolWrapper(socketPool, parent) { this.origPool = socketPool; this.fullName = socketPool.name; if (this.fullName != socketPool.type) this.fullName += ' (' + socketPool.type + ')'; if (parent) this.fullName = parent.fullName + '->' + this.fullName; } /** * Returns an array of SocketPoolWrappers created from each of the socket * pools in |socketPoolInfo|. Nested socket pools appear immediately after * their parent, and groups of nodes from trees with root nodes with the same * id are placed adjacent to each other. */ SocketPoolWrapper.createArrayFrom = function(socketPoolInfo) { // Create SocketPoolWrappers for each socket pool and separate socket pools // them into different arrays based on root node name. var socketPoolGroups = []; var socketPoolNameLists = {}; for (var i = 0; i < socketPoolInfo.length; ++i) { var name = socketPoolInfo[i].name; if (!socketPoolNameLists[name]) { socketPoolNameLists[name] = []; socketPoolGroups.push(socketPoolNameLists[name]); } addSocketPoolsToList(socketPoolNameLists[name], socketPoolInfo[i], null); } // Merge the arrays. var socketPoolList = []; for (var i = 0; i < socketPoolGroups.length; ++i) { socketPoolList = socketPoolList.concat(socketPoolGroups[i]); } return socketPoolList; }; /** * Recursively creates SocketPoolWrappers from |origPool| and all its * children and adds them all to |socketPoolList|. |parent| is the * SocketPoolWrapper for the parent of |origPool|, or null, if it's * a top level socket pool. */ function addSocketPoolsToList(socketPoolList, origPool, parent) { var socketPool = new SocketPoolWrapper(origPool, parent); socketPoolList.push(socketPool); if (origPool.nested_pools) { for (var i = 0; i < origPool.nested_pools.length; ++i) { addSocketPoolsToList(socketPoolList, origPool.nested_pools[i], socketPool); } } } /** * Returns a table printer containing information on each * SocketPoolWrapper in |socketPools|. */ SocketPoolWrapper.createTablePrinter = function(socketPools) { var tablePrinter = new TablePrinter(); tablePrinter.addHeaderCell('Name'); tablePrinter.addHeaderCell('Handed Out'); tablePrinter.addHeaderCell('Idle'); tablePrinter.addHeaderCell('Connecting'); tablePrinter.addHeaderCell('Max'); tablePrinter.addHeaderCell('Max Per Group'); tablePrinter.addHeaderCell('Generation'); for (var i = 0; i < socketPools.length; i++) { var origPool = socketPools[i].origPool; tablePrinter.addRow(); tablePrinter.addCell(socketPools[i].fullName); tablePrinter.addCell(origPool.handed_out_socket_count); var idleCell = tablePrinter.addCell(origPool.idle_socket_count); var connectingCell = tablePrinter.addCell(origPool.connecting_socket_count); if (origPool.groups) { var idleSources = []; var connectingSources = []; for (var groupName in origPool.groups) { var group = origPool.groups[groupName]; idleSources = idleSources.concat(group.idle_sockets); connectingSources = connectingSources.concat(group.connect_jobs); } idleCell.link = sourceListLink(idleSources); connectingCell.link = sourceListLink(connectingSources); } tablePrinter.addCell(origPool.max_socket_count); tablePrinter.addCell(origPool.max_sockets_per_group); tablePrinter.addCell(origPool.pool_generation_number); } return tablePrinter; }; SocketPoolWrapper.prototype = { /** * Returns a table printer containing information on all a * socket pool's groups. */ createGroupTablePrinter: function() { var tablePrinter = new TablePrinter(); tablePrinter.setTitle(this.fullName); tablePrinter.addHeaderCell('Name'); tablePrinter.addHeaderCell('Pending'); tablePrinter.addHeaderCell('Top Priority'); tablePrinter.addHeaderCell('Active'); tablePrinter.addHeaderCell('Idle'); tablePrinter.addHeaderCell('Connect Jobs'); tablePrinter.addHeaderCell('Backup Job'); tablePrinter.addHeaderCell('Stalled'); for (var groupName in this.origPool.groups) { var group = this.origPool.groups[groupName]; tablePrinter.addRow(); tablePrinter.addCell(groupName); tablePrinter.addCell(group.pending_request_count); if (group.top_pending_priority != undefined) tablePrinter.addCell(group.top_pending_priority); else tablePrinter.addCell('-'); tablePrinter.addCell(group.active_socket_count); var idleCell = tablePrinter.addCell(group.idle_sockets.length); var connectingCell = tablePrinter.addCell(group.connect_jobs.length); idleCell.link = sourceListLink(group.idle_sockets); connectingCell.link = sourceListLink(group.connect_jobs); tablePrinter.addCell(group.has_backup_job); tablePrinter.addCell(group.is_stalled); } return tablePrinter; } }; /** * Takes in a list of source IDs and returns a link that will select the * specified sources. */ function sourceListLink(sources) { if (!sources.length) return null; return '#events&q=id:' + sources.join(','); } return SocketPoolWrapper; })(); // Copyright (c) 2012 The Chromium Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. /** * This view displays information on the state of all socket pools. * * - Shows a summary of the state of each socket pool at the top. * - For each pool with allocated sockets or connect jobs, shows all its * groups with any allocated sockets. */ var SocketsView = (function() { 'use strict'; // We inherit from DivView. var superClass = DivView; /** * @constructor */ function SocketsView() { assertFirstConstructorCall(SocketsView); // Call superclass's constructor. superClass.call(this, SocketsView.MAIN_BOX_ID); g_browser.addSocketPoolInfoObserver(this, true); this.socketPoolDiv_ = $(SocketsView.SOCKET_POOL_DIV_ID); this.socketPoolGroupsDiv_ = $(SocketsView.SOCKET_POOL_GROUPS_DIV_ID); var closeIdleButton = $(SocketsView.CLOSE_IDLE_SOCKETS_BUTTON_ID); closeIdleButton.onclick = this.closeIdleSockets.bind(this); var flushSocketsButton = $(SocketsView.SOCKET_POOL_FLUSH_BUTTON_ID); flushSocketsButton.onclick = this.flushSocketPools.bind(this); } // ID for special HTML element in category_tabs.html SocketsView.TAB_HANDLE_ID = 'tab-handle-sockets'; // IDs for special HTML elements in sockets_view.html SocketsView.MAIN_BOX_ID = 'sockets-view-tab-content'; SocketsView.SOCKET_POOL_DIV_ID = 'sockets-view-pool-div'; SocketsView.SOCKET_POOL_GROUPS_DIV_ID = 'sockets-view-pool-groups-div'; SocketsView.CLOSE_IDLE_SOCKETS_BUTTON_ID = 'sockets-view-close-idle-button'; SocketsView.SOCKET_POOL_FLUSH_BUTTON_ID = 'sockets-view-flush-button'; cr.addSingletonGetter(SocketsView); SocketsView.prototype = { // Inherit the superclass's methods. __proto__: superClass.prototype, onLoadLogFinish: function(data) { return this.onSocketPoolInfoChanged(data.socketPoolInfo); }, onSocketPoolInfoChanged: function(socketPoolInfo) { this.socketPoolDiv_.innerHTML = ''; this.socketPoolGroupsDiv_.innerHTML = ''; if (!socketPoolInfo) return false; var socketPools = SocketPoolWrapper.createArrayFrom(socketPoolInfo); var tablePrinter = SocketPoolWrapper.createTablePrinter(socketPools); tablePrinter.toHTML(this.socketPoolDiv_, 'styled-table'); // Add table for each socket pool with information on each of its groups. for (var i = 0; i < socketPools.length; ++i) { if (socketPools[i].origPool.groups != undefined) { var p = addNode(this.socketPoolGroupsDiv_, 'p'); var br = addNode(p, 'br'); var groupTablePrinter = socketPools[i].createGroupTablePrinter(); groupTablePrinter.toHTML(p, 'styled-table'); } } return true; }, closeIdleSockets: function() { g_browser.sendCloseIdleSockets(); g_browser.checkForUpdatedInfo(false); }, flushSocketPools: function() { g_browser.sendFlushSocketPools(); g_browser.checkForUpdatedInfo(false); } }; return SocketsView; })(); // Copyright (c) 2012 The Chromium Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. /** * This view displays a summary of the state of each SPDY sessions, and * has links to display them in the events tab. */ var SpdyView = (function() { 'use strict'; // We inherit from DivView. var superClass = DivView; /** * @constructor */ function SpdyView() { assertFirstConstructorCall(SpdyView); // Call superclass's constructor. superClass.call(this, SpdyView.MAIN_BOX_ID); g_browser.addSpdySessionInfoObserver(this, true); g_browser.addSpdyStatusObserver(this, true); g_browser.addSpdyAlternateProtocolMappingsObserver(this, true); this.spdyEnabledSpan_ = $(SpdyView.ENABLED_SPAN_ID); this.spdyUseAlternateProtocolSpan_ = $(SpdyView.USE_ALTERNATE_PROTOCOL_SPAN_ID); this.spdyForceAlwaysSpan_ = $(SpdyView.FORCE_ALWAYS_SPAN_ID); this.spdyForceOverSslSpan_ = $(SpdyView.FORCE_OVER_SSL_SPAN_ID); this.spdyNextProtocolsSpan_ = $(SpdyView.NEXT_PROTOCOLS_SPAN_ID); this.spdyAlternateProtocolMappingsDiv_ = $(SpdyView.ALTERNATE_PROTOCOL_MAPPINGS_DIV_ID); this.spdySessionNoneSpan_ = $(SpdyView.SESSION_NONE_SPAN_ID); this.spdySessionLinkSpan_ = $(SpdyView.SESSION_LINK_SPAN_ID); this.spdySessionDiv_ = $(SpdyView.SESSION_DIV_ID); } // ID for special HTML element in category_tabs.html SpdyView.TAB_HANDLE_ID = 'tab-handle-spdy'; // IDs for special HTML elements in spdy_view.html SpdyView.MAIN_BOX_ID = 'spdy-view-tab-content'; SpdyView.ENABLED_SPAN_ID = 'spdy-view-enabled-span'; SpdyView.USE_ALTERNATE_PROTOCOL_SPAN_ID = 'spdy-view-alternate-protocol-span'; SpdyView.FORCE_ALWAYS_SPAN_ID = 'spdy-view-force-always-span'; SpdyView.FORCE_OVER_SSL_SPAN_ID = 'spdy-view-force-over-ssl-span'; SpdyView.NEXT_PROTOCOLS_SPAN_ID = 'spdy-view-next-protocols-span'; SpdyView.ALTERNATE_PROTOCOL_MAPPINGS_DIV_ID = 'spdy-view-alternate-protocol-mappings-div'; SpdyView.SESSION_NONE_SPAN_ID = 'spdy-view-session-none-span'; SpdyView.SESSION_LINK_SPAN_ID = 'spdy-view-session-link-span'; SpdyView.SESSION_DIV_ID = 'spdy-view-session-div'; cr.addSingletonGetter(SpdyView); SpdyView.prototype = { // Inherit the superclass's methods. __proto__: superClass.prototype, onLoadLogFinish: function(data) { return this.onSpdySessionInfoChanged(data.spdySessionInfo) && this.onSpdyStatusChanged(data.spdyStatus) && this.onSpdyAlternateProtocolMappingsChanged( data.spdyAlternateProtocolMappings); }, /** * If |spdySessionInfo| there are any sessions, display a single table with * information on each SPDY session. Otherwise, displays "None". */ onSpdySessionInfoChanged: function(spdySessionInfo) { this.spdySessionDiv_.innerHTML = ''; var hasNoSession = (spdySessionInfo == null || spdySessionInfo.length == 0); setNodeDisplay(this.spdySessionNoneSpan_, hasNoSession); setNodeDisplay(this.spdySessionLinkSpan_, !hasNoSession); // Only want to be hide the tab if there's no data. In the case of having // data but no sessions, still show the tab. if (!spdySessionInfo) return false; if (!hasNoSession) { var tablePrinter = createSessionTablePrinter(spdySessionInfo); tablePrinter.toHTML(this.spdySessionDiv_, 'styled-table'); } return true; }, /** * Displays information on the global SPDY status. */ onSpdyStatusChanged: function(spdyStatus) { this.spdyEnabledSpan_.textContent = spdyStatus.spdy_enabled; this.spdyUseAlternateProtocolSpan_.textContent = spdyStatus.use_alternate_protocols; this.spdyForceAlwaysSpan_.textContent = spdyStatus.force_spdy_always; this.spdyForceOverSslSpan_.textContent = spdyStatus.force_spdy_over_ssl; this.spdyNextProtocolsSpan_.textContent = spdyStatus.next_protos; return true; }, /** * If |spdyAlternateProtocolMappings| is not empty, displays a single table * with information on each alternate protocol enabled server. Otherwise, * displays "None". */ onSpdyAlternateProtocolMappingsChanged: function(spdyAlternateProtocolMappings) { this.spdyAlternateProtocolMappingsDiv_.innerHTML = ''; if (spdyAlternateProtocolMappings != null && spdyAlternateProtocolMappings.length > 0) { var tabPrinter = createAlternateProtocolMappingsTablePrinter( spdyAlternateProtocolMappings); tabPrinter.toHTML( this.spdyAlternateProtocolMappingsDiv_, 'styled-table'); } else { this.spdyAlternateProtocolMappingsDiv_.innerHTML = 'None'; } return true; } }; /** * Creates a table printer to print out the state of list of SPDY sessions. */ function createSessionTablePrinter(spdySessions) { var tablePrinter = new TablePrinter(); tablePrinter.addHeaderCell('Host'); tablePrinter.addHeaderCell('Proxy'); tablePrinter.addHeaderCell('ID'); tablePrinter.addHeaderCell('Protocol Negotiatied'); tablePrinter.addHeaderCell('Active streams'); tablePrinter.addHeaderCell('Unclaimed pushed'); tablePrinter.addHeaderCell('Max'); tablePrinter.addHeaderCell('Initiated'); tablePrinter.addHeaderCell('Pushed'); tablePrinter.addHeaderCell('Pushed and claimed'); tablePrinter.addHeaderCell('Abandoned'); tablePrinter.addHeaderCell('Received frames'); tablePrinter.addHeaderCell('Secure'); tablePrinter.addHeaderCell('Sent settings'); tablePrinter.addHeaderCell('Received settings'); tablePrinter.addHeaderCell('Error'); for (var i = 0; i < spdySessions.length; i++) { var session = spdySessions[i]; tablePrinter.addRow(); var host = session.host_port_pair; if (session.aliases) host += ' ' + session.aliases.join(' '); tablePrinter.addCell(host); tablePrinter.addCell(session.proxy); var idCell = tablePrinter.addCell(session.source_id); idCell.link = '#events&q=id:' + session.source_id; tablePrinter.addCell(session.protocol_negotiated); tablePrinter.addCell(session.active_streams); tablePrinter.addCell(session.unclaimed_pushed_streams); tablePrinter.addCell(session.max_concurrent_streams); tablePrinter.addCell(session.streams_initiated_count); tablePrinter.addCell(session.streams_pushed_count); tablePrinter.addCell(session.streams_pushed_and_claimed_count); tablePrinter.addCell(session.streams_abandoned_count); tablePrinter.addCell(session.frames_received); tablePrinter.addCell(session.is_secure); tablePrinter.addCell(session.sent_settings); tablePrinter.addCell(session.received_settings); tablePrinter.addCell(session.error); } return tablePrinter; } /** * Creates a table printer to print out the list of alternate protocol * mappings. */ function createAlternateProtocolMappingsTablePrinter( spdyAlternateProtocolMappings) { var tablePrinter = new TablePrinter(); tablePrinter.addHeaderCell('Host'); tablePrinter.addHeaderCell('Alternate Protocol'); for (var i = 0; i < spdyAlternateProtocolMappings.length; i++) { var entry = spdyAlternateProtocolMappings[i]; tablePrinter.addRow(); tablePrinter.addCell(entry.host_port_pair); tablePrinter.addCell(entry.alternate_protocol); } return tablePrinter; } return SpdyView; })(); // Copyright (c) 2012 The Chromium Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. /** * This view displays information on Winsock layered service providers and * namespace providers. * * For each layered service provider, shows the name, dll, and type * information. For each namespace provider, shows the name and * whether or not it's active. */ var ServiceProvidersView = (function() { 'use strict'; // We inherit from DivView. var superClass = DivView; /** * @constructor */ function ServiceProvidersView() { assertFirstConstructorCall(ServiceProvidersView); // Call superclass's constructor. superClass.call(this, ServiceProvidersView.MAIN_BOX_ID); this.serviceProvidersTbody_ = $(ServiceProvidersView.SERVICE_PROVIDERS_TBODY_ID); this.namespaceProvidersTbody_ = $(ServiceProvidersView.NAMESPACE_PROVIDERS_TBODY_ID); g_browser.addServiceProvidersObserver(this, true); } // ID for special HTML element in category_tabs.html ServiceProvidersView.TAB_HANDLE_ID = 'tab-handle-service-providers'; // IDs for special HTML elements in service_providers_view.html ServiceProvidersView.MAIN_BOX_ID = 'service-providers-view-tab-content'; ServiceProvidersView.SERVICE_PROVIDERS_TBODY_ID = 'service-providers-view-tbody'; ServiceProvidersView.NAMESPACE_PROVIDERS_TBODY_ID = 'service-providers-view-namespace-providers-tbody'; cr.addSingletonGetter(ServiceProvidersView); ServiceProvidersView.prototype = { // Inherit the superclass's methods. __proto__: superClass.prototype, onLoadLogFinish: function(data) { return this.onServiceProvidersChanged(data.serviceProviders); }, onServiceProvidersChanged: function(serviceProviders) { return serviceProviders && this.updateServiceProviders_(serviceProviders['service_providers']) && this.updateNamespaceProviders_( serviceProviders['namespace_providers']); }, /** * Updates the table of layered service providers. */ updateServiceProviders_: function(serviceProviders) { this.serviceProvidersTbody_.innerHTML = ''; if (!serviceProviders) return false; // Add a table row for each service provider. for (var i = 0; i < serviceProviders.length; ++i) { var tr = addNode(this.serviceProvidersTbody_, 'tr'); var entry = serviceProviders[i]; addNodeWithText(tr, 'td', entry.name); addNodeWithText(tr, 'td', entry.version); addNodeWithText(tr, 'td', getLayeredServiceProviderType(entry)); addNodeWithText(tr, 'td', getSocketType(entry)); addNodeWithText(tr, 'td', getProtocolType(entry)); addNodeWithText(tr, 'td', entry.path); } return true; }, /** * Updates the lable of namespace providers. */ updateNamespaceProviders_: function(namespaceProviders) { this.namespaceProvidersTbody_.innerHTML = ''; if (!namespaceProviders) return false; // Add a table row for each namespace provider. for (var i = 0; i < namespaceProviders.length; ++i) { var tr = addNode(this.namespaceProvidersTbody_, 'tr'); var entry = namespaceProviders[i]; addNodeWithText(tr, 'td', entry.name); addNodeWithText(tr, 'td', entry.version); addNodeWithText(tr, 'td', getNamespaceProviderType(entry)); addNodeWithText(tr, 'td', entry.active); } return true; } }; /** * Returns type of a layered service provider. */ function getLayeredServiceProviderType(serviceProvider) { if (serviceProvider.chain_length == 0) return 'Layer'; if (serviceProvider.chain_length == 1) return 'Base'; return 'Chain'; } var NAMESPACE_PROVIDER_PTYPE = { '12': 'NS_DNS', '15': 'NS_NLA', '16': 'NS_BTH', '32': 'NS_NTDS', '37': 'NS_EMAIL', '38': 'NS_PNRPNAME', '39': 'NS_PNRPCLOUD' }; /** * Returns the type of a namespace provider as a string. */ function getNamespaceProviderType(namespaceProvider) { return tryGetValueWithKey(NAMESPACE_PROVIDER_PTYPE, namespaceProvider.type); }; var SOCKET_TYPE = { '1': 'SOCK_STREAM', '2': 'SOCK_DGRAM', '3': 'SOCK_RAW', '4': 'SOCK_RDM', '5': 'SOCK_SEQPACKET' }; /** * Returns socket type of a layered service provider as a string. */ function getSocketType(layeredServiceProvider) { return tryGetValueWithKey(SOCKET_TYPE, layeredServiceProvider.socket_type); } var PROTOCOL_TYPE = { '1': 'IPPROTO_ICMP', '6': 'IPPROTO_TCP', '17': 'IPPROTO_UDP', '58': 'IPPROTO_ICMPV6' }; /** * Returns protocol type of a layered service provider as a string. */ function getProtocolType(layeredServiceProvider) { return tryGetValueWithKey(PROTOCOL_TYPE, layeredServiceProvider.socket_protocol); } return ServiceProvidersView; })(); // Copyright (c) 2012 The Chromium Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. /** * This view displays network related log data and is specific fo ChromeOS. * We get log data from chrome by filtering system logs for network related * keywords. Logs are not fetched until we actually need them. */ var LogsView = (function() { 'use strict'; // Special classes (defined in logs_view.css). var LOG_ROW_COLLAPSED_CLASSNAME = 'logs-view-log-row-collapsed'; var LOG_ROW_EXPANDED_CLASSNAME = 'logs-view-log-row-expanded'; var LOG_CELL_TEXT_CLASSNAME = 'logs-view-log-cell-text'; var LOG_CELL_LOG_CLASSNAME = 'logs-view-log-cell-log'; var LOG_TABLE_BUTTON_COLUMN_CLASSNAME = 'logs-view-log-table-button-column'; var LOG_BUTTON_CLASSNAME = 'logs-view-log-button'; // We inherit from DivView. var superClass = DivView; /** * @constructor */ function LogsView() { assertFirstConstructorCall(LogsView); // Call superclass's constructor. superClass.call(this, LogsView.MAIN_BOX_ID); var tableDiv = $(LogsView.TABLE_ID); this.rows = []; this.populateTable(tableDiv, LOG_FILTER_LIST); $(LogsView.GLOBAL_SHOW_BUTTON_ID).addEventListener('click', this.onGlobalChangeVisibleClick_.bind(this, true)); $(LogsView.GLOBAL_HIDE_BUTTON_ID).addEventListener('click', this.onGlobalChangeVisibleClick_.bind(this, false)); $(LogsView.REFRESH_LOGS_BUTTON_ID).addEventListener('click', this.onLogsRefresh_.bind(this)); } // ID for special HTML element in category_tabs.html LogsView.TAB_HANDLE_ID = 'tab-handle-logs'; // IDs for special HTML elements in logs_view.html LogsView.MAIN_BOX_ID = 'logs-view-tab-content'; LogsView.TABLE_ID = 'logs-view-log-table'; LogsView.GLOBAL_SHOW_BUTTON_ID = 'logs-view-global-show-btn'; LogsView.GLOBAL_HIDE_BUTTON_ID = 'logs-view-global-hide-btn'; LogsView.REFRESH_LOGS_BUTTON_ID = 'logs-view-refresh-btn'; cr.addSingletonGetter(LogsView); /** * Contains log keys we are interested in. */ var LOG_FILTER_LIST = [ { key: 'syslog', }, { key: 'ui_log', }, { key: 'chrome_system_log', }, { key: 'chrome_log', } ]; LogsView.prototype = { // Inherit the superclass's methods. __proto__: superClass.prototype, /** * Called during View's initialization. Creates the row of a table logs will * be shown in. Each row has 4 cells. * * First cell's content will be set to |logKey|, second will contain a * button that will be used to show or hide third cell, which will contain * the filtered log. * |logKey| also tells us which log we are getting data from. */ createTableRow: function(logKey) { var row = document.createElement('tr'); var cells = []; for (var i = 0; i < 3; i++) { var rowCell = document.createElement('td'); cells.push(rowCell); row.appendChild(rowCell); } // Log key cell. cells[0].className = LOG_CELL_TEXT_CLASSNAME; cells[0].textContent = logKey; // Cell log is displayed in. Log content is in div element that is // initially hidden and empty. cells[2].className = LOG_CELL_TEXT_CLASSNAME; var logDiv = document.createElement('div'); logDiv.textContent = ''; logDiv.className = LOG_CELL_LOG_CLASSNAME; logDiv.id = 'logs-view.log-cell.' + this.rows.length; cells[2].appendChild(logDiv); // Button that we use to show or hide div element with log content. Logs // are not visible initially, so we initialize button accordingly. var expandButton = document.createElement('button'); expandButton.textContent = 'Show...'; expandButton.className = LOG_BUTTON_CLASSNAME; expandButton.addEventListener('click', this.onButtonClicked_.bind(this, row)); // Cell that contains show/hide button. cells[1].appendChild(expandButton); cells[1].className = LOG_TABLE_BUTTON_COLUMN_CLASSNAME; // Initially, log is not visible. row.className = LOG_ROW_COLLAPSED_CLASSNAME; // We will need those to process row buttons' onclick events. row.logKey = logKey; row.expandButton = expandButton; row.logDiv = logDiv; row.logVisible = false; this.rows.push(row); return row; }, /** * Initializes |tableDiv| to represent data from |logList| which should be * of type LOG_FILTER_LIST. */ populateTable: function(tableDiv, logList) { for (var i = 0; i < logList.length; i++) { var logSource = this.createTableRow(logList[i].key); tableDiv.appendChild(logSource); } }, /** * Processes clicks on buttons that show or hide log contents in log row. * Row containing the clicked button is given to the method since it * contains all data we need to process the click (unlike button object * itself). */ onButtonClicked_: function(containingRow) { if (!containingRow.logVisible) { containingRow.className = LOG_ROW_EXPANDED_CLASSNAME; containingRow.expandButton.textContent = 'Hide...'; var logDiv = containingRow.logDiv; if (logDiv.textContent == '') { logDiv.textContent = 'Getting logs...'; // Callback will be executed by g_browser. g_browser.getSystemLog(containingRow.logKey, containingRow.logDiv.id); } } else { containingRow.className = LOG_ROW_COLLAPSED_CLASSNAME; containingRow.expandButton.textContent = 'Show...'; } containingRow.logVisible = !containingRow.logVisible; }, /** * Processes click on one of the buttons that are used to show or hide all * logs we care about. */ onGlobalChangeVisibleClick_: function(isShowAll) { for (var row in this.rows) { if (isShowAll != this.rows[row].logVisible) { this.onButtonClicked_(this.rows[row]); } } }, /** * Processes click event on the button we use to refresh fetched logs. We * get the newest logs from libcros, and refresh content of the visible log * cells. */ onLogsRefresh_: function() { g_browser.refreshSystemLogs(); var visibleLogRows = []; var hiddenLogRows = []; for (var row in this.rows) { if (this.rows[row].logVisible) { visibleLogRows.push(this.rows[row]); } else { hiddenLogRows.push(this.rows[row]); } } // We have to refresh text content in visible rows. for (row in visibleLogRows) { visibleLogRows[row].logDiv.textContent = 'Getting logs...'; g_browser.getSystemLog(visibleLogRows[row].logKey, visibleLogRows[row].logDiv.id); } // In hidden rows we just clear potential log text, so we know we have to // get new contents when we show the row next time. for (row in hiddenLogRows) { hiddenLogRows[row].logDiv.textContent = ''; } } }; return LogsView; })(); // Copyright (c) 2012 The Chromium Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. /** * This view displays information related to Prerendering. */ var PrerenderView = (function() { 'use strict'; // We inherit from DivView. var superClass = DivView; /** * @constructor */ function PrerenderView() { assertFirstConstructorCall(PrerenderView); // Call superclass's constructor. superClass.call(this, PrerenderView.MAIN_BOX_ID); g_browser.addPrerenderInfoObserver(this, true); this.prerenderEnabledSpan_ = $(PrerenderView.ENABLED_SPAN_ID); this.prerenderOmniboxEnabledSpan_ = $(PrerenderView.OMNIBOX_ENABLED_SPAN_ID); this.prerenderHistoryDiv_ = $(PrerenderView.HISTORY_DIV_ID); this.prerenderActiveDiv_ = $(PrerenderView.ACTIVE_DIV_ID); } // ID for special HTML element in category_tabs.html PrerenderView.TAB_HANDLE_ID = 'tab-handle-prerender'; // IDs for special HTML elements in prerender_view.html PrerenderView.MAIN_BOX_ID = 'prerender-view-tab-content'; PrerenderView.ENABLED_SPAN_ID = 'prerender-view-enabled-span'; PrerenderView.OMNIBOX_ENABLED_SPAN_ID = 'prerender-view-omnibox-enabled-span'; PrerenderView.HISTORY_DIV_ID = 'prerender-view-history-div'; PrerenderView.ACTIVE_DIV_ID = 'prerender-view-active-div'; cr.addSingletonGetter(PrerenderView); PrerenderView.prototype = { // Inherit the superclass's methods. __proto__: superClass.prototype, onLoadLogFinish: function(data) { return this.onPrerenderInfoChanged(data.prerenderInfo); }, onPrerenderInfoChanged: function(prerenderInfo) { this.prerenderEnabledSpan_.textContent = ''; this.prerenderOmniboxEnabledSpan_.textContent = ''; this.prerenderHistoryDiv_.innerHTML = ''; this.prerenderActiveDiv_.innerHTML = ''; if (prerenderInfo && ('enabled' in prerenderInfo)) { this.prerenderEnabledSpan_.textContent = prerenderInfo.enabled.toString(); if (prerenderInfo.enabled_note) { this.prerenderEnabledSpan_.textContent += ' ' + prerenderInfo.enabled_note; } } if (prerenderInfo && ('omnibox_enabled' in prerenderInfo)) { this.prerenderOmniboxEnabledSpan_.textContent = prerenderInfo.omnibox_enabled.toString(); } if (!isValidPrerenderInfo(prerenderInfo)) return false; var tabPrinter = createHistoryTablePrinter(prerenderInfo.history); tabPrinter.toHTML(this.prerenderHistoryDiv_, 'styled-table'); var tabPrinter = createActiveTablePrinter(prerenderInfo.active); tabPrinter.toHTML(this.prerenderActiveDiv_, 'styled-table'); return true; } }; function isValidPrerenderInfo(prerenderInfo) { if (prerenderInfo == null) { return false; } if (!('history' in prerenderInfo) || !('active' in prerenderInfo) || !('enabled' in prerenderInfo)) { return false; } return true; } function createHistoryTablePrinter(prerenderHistory) { var tablePrinter = new TablePrinter(); tablePrinter.addHeaderCell('Origin'); tablePrinter.addHeaderCell('URL'); tablePrinter.addHeaderCell('Final Status'); tablePrinter.addHeaderCell('Time'); for (var i = 0; i < prerenderHistory.length; i++) { var historyEntry = prerenderHistory[i]; tablePrinter.addRow(); tablePrinter.addCell(historyEntry.origin); tablePrinter.addCell(historyEntry.url); tablePrinter.addCell(historyEntry.final_status); var date = new Date(parseInt(historyEntry.end_time)); // TODO(eroman): Switch to addNodeWithDate() tablePrinter.addCell(timeutil.dateToString(date)); } return tablePrinter; } function createActiveTablePrinter(prerenderActive) { var tablePrinter = new TablePrinter(); tablePrinter.addHeaderCell('URL'); tablePrinter.addHeaderCell('Duration'); for (var i = 0; i < prerenderActive.length; i++) { var activeEntry = prerenderActive[i]; tablePrinter.addRow(); tablePrinter.addCell(activeEntry.url); tablePrinter.addCell(activeEntry.duration); } return tablePrinter; } return PrerenderView; })(); // Copyright (c) 2012 The Chromium Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. /** * This view displays information on ChromeOS specific features. */ var CrosView = (function() { 'use strict'; var fileContent; var passcode = ''; /** * Clear file input div * * @private */ function clearFileInput_() { $(CrosView.IMPORT_DIV_ID).innerHTML = $(CrosView.IMPORT_DIV_ID).innerHTML; $(CrosView.IMPORT_ONC_ID).addEventListener('change', handleFileChangeEvent_, false); } /** * Send file contents and passcode to C++ cros network library. * * @private */ function importONCFile_() { clearParseStatus_(); if (fileContent) g_browser.importONCFile(fileContent, passcode); else setParseStatus_('ONC file parse failed: cannot read file'); clearFileInput_(); } /** * Set the passcode var, and trigger onc import. * * @param {string} value The passcode value. * @private */ function setPasscode_(value) { passcode = value; if (passcode) importONCFile_(); } /** * Unhide the passcode prompt input field and give it focus. * * @private */ function promptForPasscode_() { $(CrosView.PASSCODE_ID).hidden = false; $(CrosView.PASSCODE_INPUT_ID).focus(); $(CrosView.PASSCODE_INPUT_ID).select(); } /** * Set the fileContent var, and trigger onc import if the file appears to * not be encrypted, or prompt for passcode if the file is encrypted. * * @private * @param {string} text contents of selected file. */ function setFileContent_(result) { fileContent = result; // Parse the JSON to get at the top level "Type" property. var json_object; // Ignore any parse errors: they'll get handled in the C++ import code. try { json_object = JSON.parse(fileContent); } catch (error) {} // Check if file is encrypted. if (json_object && json_object.hasOwnProperty('Type') && json_object.Type == 'EncryptedConfiguration') { promptForPasscode_(); } else { importONCFile_(); } } /** * Clear ONC file parse status. Clears and hides the parse status div. * * @private */ function clearParseStatus_(error) { var parseStatus = $(CrosView.PARSE_STATUS_ID); parseStatus.hidden = true; parseStatus.textContent = ''; } /** * Set ONC file parse status. * * @private */ function setParseStatus_(error) { var parseStatus = $(CrosView.PARSE_STATUS_ID); parseStatus.hidden = false; parseStatus.textContent = error ? 'ONC file parse failed: ' + error : 'ONC file successfully parsed'; reset_(); } /** * Set storing debug logs status. * * @private */ function setStoreDebugLogsStatus_(status) { $(CrosView.STORE_DEBUG_LOGS_STATUS_ID).innerText = status; } /** * Set status for current debug mode. * * @private */ function setNetworkDebugModeStatus_(status) { $(CrosView.DEBUG_STATUS_ID).innerText = status; } /** * An event listener for the file selection field. * * @private */ function handleFileChangeEvent_(event) { clearParseStatus_(); var file = event.target.files[0]; var reader = new FileReader(); reader.onloadend = function(e) { setFileContent_(reader.result); }; reader.readAsText(file); } /** * Add event listeners for the file selection, passcode input * fields, for the button for debug logs storing and for buttons * for debug mode selection. * * @private */ function addEventListeners_() { $(CrosView.IMPORT_ONC_ID).addEventListener('change', handleFileChangeEvent_, false); $(CrosView.PASSCODE_INPUT_ID).addEventListener('change', function(event) { setPasscode_(this.value); }, false); $(CrosView.STORE_DEBUG_LOGS_ID).addEventListener('click', function(event) { $(CrosView.STORE_DEBUG_LOGS_STATUS_ID).innerText = ''; g_browser.storeDebugLogs(); }, false); $(CrosView.DEBUG_WIFI_ID).addEventListener('click', function(event) { setNetworkDebugMode_('wifi'); }, false); $(CrosView.DEBUG_ETHERNET_ID).addEventListener('click', function(event) { setNetworkDebugMode_('ethernet'); }, false); $(CrosView.DEBUG_CELLULAR_ID).addEventListener('click', function(event) { setNetworkDebugMode_('cellular'); }, false); $(CrosView.DEBUG_WIMAX_ID).addEventListener('click', function(event) { setNetworkDebugMode_('wimax'); }, false); $(CrosView.DEBUG_NONE_ID).addEventListener('click', function(event) { setNetworkDebugMode_('none'); }, false); } /** * Reset fileContent and passcode vars. * * @private */ function reset_() { fileContent = undefined; passcode = ''; $(CrosView.PASSCODE_ID).hidden = true; } /** * Enables or disables debug mode for a specified subsystem. * * @private */ function setNetworkDebugMode_(subsystem) { $(CrosView.DEBUG_STATUS_ID).innerText = ''; g_browser.setNetworkDebugMode(subsystem); } /** * @constructor * @extends {DivView} */ function CrosView() { assertFirstConstructorCall(CrosView); // Call superclass's constructor. DivView.call(this, CrosView.MAIN_BOX_ID); g_browser.addCrosONCFileParseObserver(this); g_browser.addStoreDebugLogsObserver(this); g_browser.addSetNetworkDebugModeObserver(this); addEventListeners_(); } // ID for special HTML element in category_tabs.html CrosView.TAB_HANDLE_ID = 'tab-handle-chromeos'; CrosView.MAIN_BOX_ID = 'chromeos-view-tab-content'; CrosView.IMPORT_DIV_ID = 'chromeos-view-import-div'; CrosView.IMPORT_ONC_ID = 'chromeos-view-import-onc'; CrosView.PASSCODE_ID = 'chromeos-view-password-div'; CrosView.PASSCODE_INPUT_ID = 'chromeos-view-onc-password'; CrosView.PARSE_STATUS_ID = 'chromeos-view-parse-status'; CrosView.STORE_DEBUG_LOGS_ID = 'chromeos-view-store-debug-logs'; CrosView.STORE_DEBUG_LOGS_STATUS_ID = 'chromeos-view-store-debug-logs-status'; CrosView.DEBUG_WIFI_ID = 'chromeos-view-network-debugging-wifi'; CrosView.DEBUG_ETHERNET_ID = 'chromeos-view-network-debugging-ethernet'; CrosView.DEBUG_CELLULAR_ID = 'chromeos-view-network-debugging-cellular'; CrosView.DEBUG_WIMAX_ID = 'chromeos-view-network-debugging-wimax'; CrosView.DEBUG_NONE_ID = 'chromeos-view-network-debugging-none'; CrosView.DEBUG_STATUS_ID = 'chromeos-view-network-debugging-status'; cr.addSingletonGetter(CrosView); CrosView.prototype = { // Inherit from DivView. __proto__: DivView.prototype, onONCFileParse: setParseStatus_, onStoreDebugLogs: setStoreDebugLogsStatus_, onSetNetworkDebugMode: setNetworkDebugModeStatus_, }; return CrosView; })(); // Copyright (c) 2012 The Chromium Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. /** * This view displays a summary of the state of each HTTP pipelined connection, * and has links to display them in the events tab. */ var HttpPipelineView = (function() { 'use strict'; // We inherit from DivView. var superClass = DivView; /** * @constructor */ function HttpPipelineView() { assertFirstConstructorCall(HttpPipelineView); // Call superclass's constructor. superClass.call(this, HttpPipelineView.MAIN_BOX_ID); g_browser.addHttpPipeliningStatusObserver(this, true); this.httpPipeliningEnabledSpan_ = $(HttpPipelineView.ENABLED_SPAN_ID); this.httpPipelineConnectionsNoneSpan_ = $(HttpPipelineView.CONNECTIONS_NONE_SPAN_ID); this.httpPipelineConnectionsLinkSpan_ = $(HttpPipelineView.CONNECTIONS_LINK_SPAN_ID); this.httpPipelineConnectionsDiv_ = $(HttpPipelineView.CONNECTIONS_DIV_ID); this.httpPipelineKnownHostsDiv_ = $(HttpPipelineView.KNOWN_HOSTS_DIV_ID); } // ID for special HTML element in category_tabs.html HttpPipelineView.TAB_HANDLE_ID = 'tab-handle-http-pipeline'; // IDs for special HTML elements in http_pipeline_view.html HttpPipelineView.MAIN_BOX_ID = 'http-pipeline-view-tab-content'; HttpPipelineView.ENABLED_SPAN_ID = 'http-pipeline-view-enabled-span'; HttpPipelineView.CONNECTIONS_NONE_SPAN_ID = 'http-pipeline-view-connections-none-span'; HttpPipelineView.CONNECTIONS_LINK_SPAN_ID = 'http-pipeline-view-connections-link-span'; HttpPipelineView.CONNECTIONS_DIV_ID = 'http-pipeline-view-connections-div'; HttpPipelineView.KNOWN_HOSTS_DIV_ID = 'http-pipeline-view-known-hosts-div'; cr.addSingletonGetter(HttpPipelineView); HttpPipelineView.prototype = { // Inherit the superclass's methods. __proto__: superClass.prototype, onLoadLogFinish: function(data) { return this.onHttpPipeliningStatusChanged(data.httpPipeliningStatus); }, /** * Displays information on the global HTTP pipelining status. */ onHttpPipeliningStatusChanged: function(httpPipelineStatus) { return this.displayHttpPipeliningEnabled(httpPipelineStatus) && this.displayHttpPipelinedConnectionInfo( httpPipelineStatus.pipelined_connection_info) && this.displayHttpPipeliningKnownHosts( httpPipelineStatus.pipelined_host_info); }, displayHttpPipeliningEnabled: function(httpPipelineStatus) { this.httpPipeliningEnabledSpan_.textContent = httpPipelineStatus.pipelining_enabled; return httpPipelineStatus.pipelining_enabled; }, /** * If |httpPipelinedConnectionInfo| is not empty, then display information * on each HTTP pipelined connection. Otherwise, displays "None". */ displayHttpPipelinedConnectionInfo: function(httpPipelinedConnectionInfo) { this.httpPipelineConnectionsDiv_.innerHTML = ''; var hasInfo = (httpPipelinedConnectionInfo != null && httpPipelinedConnectionInfo.length > 0); setNodeDisplay(this.httpPipelineConnectionsNoneSpan_, !hasInfo); setNodeDisplay(this.httpPipelineConnectionsLinkSpan_, hasInfo); if (hasInfo) { var tablePrinter = createConnectionTablePrinter( httpPipelinedConnectionInfo); tablePrinter.toHTML(this.httpPipelineConnectionsDiv_, 'styled-table'); } return true; }, /** * If |httpPipeliningKnownHosts| is not empty, displays a single table * with information on known pipelining hosts. Otherwise, displays "None". */ displayHttpPipeliningKnownHosts: function(httpPipeliningKnownHosts) { this.httpPipelineKnownHostsDiv_.innerHTML = ''; if (httpPipeliningKnownHosts != null && httpPipeliningKnownHosts.length > 0) { var tabPrinter = createKnownHostsTablePrinter(httpPipeliningKnownHosts); tabPrinter.toHTML( this.httpPipelineKnownHostsDiv_, 'styled-table'); } else { this.httpPipelineKnownHostsDiv_.innerHTML = 'None'; } return true; } }; /** * Creates a table printer to print out the state of a list of HTTP pipelined * connections. */ function createConnectionTablePrinter(httpPipelinedConnectionInfo) { var tablePrinter = new TablePrinter(); tablePrinter.addHeaderCell('Host'); tablePrinter.addHeaderCell('Forced'); tablePrinter.addHeaderCell('Depth'); tablePrinter.addHeaderCell('Capacity'); tablePrinter.addHeaderCell('Usable'); tablePrinter.addHeaderCell('Active'); tablePrinter.addHeaderCell('ID'); for (var i = 0; i < httpPipelinedConnectionInfo.length; i++) { var host = httpPipelinedConnectionInfo[i]; for (var j = 0; j < host.length; j++) { var connection = host[j]; tablePrinter.addRow(); tablePrinter.addCell(connection.host); tablePrinter.addCell( connection.forced === undefined ? false : connection.forced); tablePrinter.addCell(connection.depth); tablePrinter.addCell(connection.capacity); tablePrinter.addCell(connection.usable); tablePrinter.addCell(connection.active); var idCell = tablePrinter.addCell(connection.source_id); idCell.link = '#events&q=id:' + connection.source_id; } } return tablePrinter; } /** * Creates a table printer to print out the list of known hosts and whether or * not they support pipelining. */ function createKnownHostsTablePrinter(httpPipeliningKnownHosts) { var tablePrinter = new TablePrinter(); tablePrinter.addHeaderCell('Host'); tablePrinter.addHeaderCell('Pipelining Capalibility'); for (var i = 0; i < httpPipeliningKnownHosts.length; i++) { var entry = httpPipeliningKnownHosts[i]; tablePrinter.addRow(); tablePrinter.addCell(entry.host); tablePrinter.addCell(entry.capability); } return tablePrinter; } return HttpPipelineView; })(); // Copyright (c) 2012 The Chromium Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. /** This view displays summary statistics on bandwidth usage. */ var BandwidthView = (function() { 'use strict'; // We inherit from DivView. var superClass = DivView; /** * @constructor */ function BandwidthView() { assertFirstConstructorCall(BandwidthView); // Call superclass's constructor. superClass.call(this, BandwidthView.MAIN_BOX_ID); g_browser.addSessionNetworkStatsObserver(this, true); g_browser.addHistoricNetworkStatsObserver(this, true); this.bandwidthUsageTable_ = $(BandwidthView.BANDWIDTH_USAGE_TABLE); this.sessionNetworkStats_ = null; this.historicNetworkStats_ = null; } // ID for special HTML element in category_tabs.html BandwidthView.TAB_HANDLE_ID = 'tab-handle-bandwidth'; // IDs for special HTML elements in bandwidth_view.html BandwidthView.MAIN_BOX_ID = 'bandwidth-view-tab-content'; BandwidthView.BANDWIDTH_USAGE_TABLE = 'bandwidth-usage-table'; cr.addSingletonGetter(BandwidthView); BandwidthView.prototype = { // Inherit the superclass's methods. __proto__: superClass.prototype, onLoadLogFinish: function(data) { return this.onSessionNetworkStatsChanged(data.sessionNetworkStats) && this.onHistoricNetworkStatsChanged(data.historicNetworkStats); }, /** * Retains information on bandwidth usage this session. */ onSessionNetworkStatsChanged: function(sessionNetworkStats) { this.sessionNetworkStats_ = sessionNetworkStats; this.updateBandwidthUsageTable(); return true; }, /** * Displays information on bandwidth usage this session and over the * browser's lifetime. */ onHistoricNetworkStatsChanged: function(historicNetworkStats) { this.historicNetworkStats_ = historicNetworkStats; this.updateBandwidthUsageTable(); return true; }, /** * Update the bandwidth usage table. */ updateBandwidthUsageTable: function() { this.bandwidthUsageTable_.innerHTML = ''; var tabPrinter = createBandwidthUsageTablePrinter( this.sessionNetworkStats_, this.historicNetworkStats_); tabPrinter.toHTML(this.bandwidthUsageTable_, 'styled-table'); return true; } }; /** * Converts bytes to kilobytes rounded to one decimal place. */ function bytesToRoundedKilobytes(val) { return (val / 1024).toFixed(1); } /** * Returns bandwidth savings as a percent rounded to one decimal place. */ function getPercentSavings(original, received) { if (original > 0) { return ((original - received) * 100 / original).toFixed(1); } return '0.0'; } /** * Adds a row of bandwidth usage statistics to the bandwidth usage table. */ function addRow(tablePrinter, title, sessionValue, historicValue) { tablePrinter.addRow(); tablePrinter.addCell(title); tablePrinter.addCell(sessionValue); tablePrinter.addCell(historicValue); } /** * Creates a table printer to print out the bandwidth usage statistics. */ function createBandwidthUsageTablePrinter(sessionNetworkStats, historicNetworkStats) { var tablePrinter = new TablePrinter(); tablePrinter.addHeaderCell(''); tablePrinter.addHeaderCell('Session'); tablePrinter.addHeaderCell('Total'); var sessionOriginal = 0; var historicOriginal = 0; var sessionReceived = 0; var historicReceived = 0; if (sessionNetworkStats != null) { sessionOriginal = sessionNetworkStats.session_original_content_length; sessionReceived = sessionNetworkStats.session_received_content_length; } if (historicNetworkStats != null) { historicOriginal = historicNetworkStats.historic_original_content_length; historicReceived = historicNetworkStats.historic_received_content_length; } addRow(tablePrinter, 'Original (KB)', bytesToRoundedKilobytes(sessionOriginal), bytesToRoundedKilobytes(historicOriginal)); addRow(tablePrinter, 'Received (KB)', bytesToRoundedKilobytes(sessionReceived), bytesToRoundedKilobytes(historicReceived)); addRow(tablePrinter, 'Savings (KB)', bytesToRoundedKilobytes(sessionOriginal - sessionReceived), bytesToRoundedKilobytes(historicOriginal - historicReceived)); addRow(tablePrinter, 'Savings (%)', getPercentSavings(sessionOriginal, sessionReceived), getPercentSavings(historicOriginal, historicReceived)); return tablePrinter; } return BandwidthView; })(); document.addEventListener('DOMContentLoaded', function() { MainView.getInstance(); // from main.js });